Merge branch 'devlopment' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web into devlopment
This commit is contained in:
commit
a3f30bf2d0
|
@ -4,8 +4,8 @@ VITE_API_PREFIX = '/dev-api'
|
|||
|
||||
# 接口地址
|
||||
# VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
|
||||
# VITE_API_BASE_URL = 'http://localhost:8888/'
|
||||
VITE_API_BASE_URL = 'http://10.18.34.163:8888/'
|
||||
VITE_API_BASE_URL = 'http://localhost:8888/'
|
||||
# VITE_API_BASE_URL = 'http://10.18.34.163:8888/'
|
||||
# VITE_API_BASE_URL = 'http://10.18.34.213:8888/'
|
||||
|
||||
# 接口地址 (WebSocket)
|
||||
|
|
|
@ -50,3 +50,23 @@ export function returnEquipment(equipmentId: string) {
|
|||
|
||||
// 导出设备采购 API
|
||||
export * from './procurement'
|
||||
|
||||
// 设备盘库相关 API
|
||||
/** @desc 分页查询设备盘库记录 */
|
||||
export function pageEquipmentInventory(query: T.EquipmentPageQuery) {
|
||||
return http.get<T.EquipmentResp[]>(`${BASE_URL}/inventory/page`, query)
|
||||
}
|
||||
|
||||
/** @desc 执行设备盘库 */
|
||||
export function executeEquipmentInventory(equipmentId: string, inventoryResult: string, remark?: string) {
|
||||
return http.post(`${BASE_URL}/inventory/${equipmentId}`, null, {
|
||||
params: { inventoryResult, remark }
|
||||
})
|
||||
}
|
||||
|
||||
/** @desc 批量执行设备盘库 */
|
||||
export function batchExecuteEquipmentInventory(equipmentIds: string[], inventoryResult: string, remark?: string) {
|
||||
return http.post(`${BASE_URL}/inventory/batch`, null, {
|
||||
params: { equipmentIds, inventoryResult, remark }
|
||||
})
|
||||
}
|
|
@ -99,6 +99,37 @@ export const equipmentProcurementApi = {
|
|||
return http.get<ApiRes<EquipmentResp>>(`/equipment/procurement/detail/${equipmentId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认收货(扩展版,支持完整设备数据)
|
||||
*/
|
||||
receiveGoods: (equipmentId: string, data: ReceiptRequest) => {
|
||||
console.log('📦 收货API被调用,设备ID:', equipmentId)
|
||||
console.log('📦 收货数据:', data)
|
||||
|
||||
return http.post<ApiRes<null>>(`/equipment/procurement/receipt/${equipmentId}`, data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取收货详情
|
||||
*/
|
||||
getReceiptDetail: (equipmentId: string) => {
|
||||
return http.get<ApiRes<ReceiptDetail>>(`/equipment/procurement/receipt/${equipmentId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行付款
|
||||
*/
|
||||
makePayment: (equipmentId: string, data: PaymentRequest) => {
|
||||
return http.post<ApiRes<null>>(`/equipment/procurement/payment/${equipmentId}`, data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取支付详情
|
||||
*/
|
||||
getPaymentDetail: (equipmentId: string) => {
|
||||
return http.get<ApiRes<PaymentDetail>>(`/equipment/procurement/payment/${equipmentId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取采购统计信息
|
||||
*/
|
||||
|
@ -117,9 +148,6 @@ export const equipmentProcurementApi = {
|
|||
* 导出设备采购记录
|
||||
*/
|
||||
export: (params: EquipmentListReq) => {
|
||||
return http.get<Blob>('/equipment/procurement/export', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
return http.get('/equipment/procurement/export', params, { responseType: 'blob' })
|
||||
}
|
||||
}
|
|
@ -101,28 +101,28 @@ export interface EquipmentResp {
|
|||
/** 资产编号 */
|
||||
assetCode?: string
|
||||
/** 设备名称 */
|
||||
equipmentName: string
|
||||
equipmentName?: string
|
||||
/** 设备类型 */
|
||||
equipmentType: string
|
||||
/** 设备类型描述 */
|
||||
equipmentType?: string
|
||||
/** 设备类型标签 */
|
||||
equipmentTypeLabel?: string
|
||||
/** 设备型号 */
|
||||
equipmentModel: string
|
||||
/** 设备SN */
|
||||
equipmentSn: string
|
||||
equipmentModel?: string
|
||||
/** 设备序列号 */
|
||||
equipmentSn?: string
|
||||
/** 品牌 */
|
||||
brand?: string
|
||||
/** 配置规格/参数 */
|
||||
specification?: string
|
||||
/** 设备状态 */
|
||||
equipmentStatus: string
|
||||
/** 设备状态描述 */
|
||||
equipmentStatus?: string
|
||||
/** 设备状态标签 */
|
||||
equipmentStatusLabel?: string
|
||||
/** 使用状态 */
|
||||
useStatus: string
|
||||
useStatus?: string
|
||||
/** 位置状态 */
|
||||
locationStatus?: string
|
||||
/** 位置状态描述 */
|
||||
/** 位置状态标签 */
|
||||
locationStatusLabel?: string
|
||||
/** 设备当前物理位置 */
|
||||
physicalLocation?: string
|
||||
|
@ -130,7 +130,7 @@ export interface EquipmentResp {
|
|||
responsiblePerson?: string
|
||||
/** 健康状态 */
|
||||
healthStatus?: string
|
||||
/** 健康状态描述 */
|
||||
/** 健康状态标签 */
|
||||
healthStatusLabel?: string
|
||||
/** 采购时间 */
|
||||
purchaseTime?: string
|
||||
|
@ -138,13 +138,13 @@ export interface EquipmentResp {
|
|||
inStockTime?: string
|
||||
/** 启用时间 */
|
||||
activationTime?: string
|
||||
/** 预计报废时间 */
|
||||
/** 预期报废时间 */
|
||||
expectedScrapTime?: string
|
||||
/** 实际报废时间 */
|
||||
actualScrapTime?: string
|
||||
/** 状态变更时间 */
|
||||
statusChangeTime?: string
|
||||
/** 采购订单 */
|
||||
/** 采购订单号 */
|
||||
purchaseOrder?: string
|
||||
/** 供应商名称 */
|
||||
supplierName?: string
|
||||
|
@ -158,9 +158,9 @@ export interface EquipmentResp {
|
|||
depreciationYears?: number
|
||||
/** 残值 */
|
||||
salvageValue?: number
|
||||
/** 保修截止日期 */
|
||||
/** 保修到期日期 */
|
||||
warrantyExpireDate?: string
|
||||
/** 上次维护日期 */
|
||||
/** 最后维护日期 */
|
||||
lastMaintenanceDate?: string
|
||||
/** 下次维护日期 */
|
||||
nextMaintenanceDate?: string
|
||||
|
@ -176,7 +176,7 @@ export interface EquipmentResp {
|
|||
projectName?: string
|
||||
/** 使用人ID */
|
||||
userId?: string
|
||||
/** 使用人 */
|
||||
/** 使用人姓名 */
|
||||
name?: string
|
||||
/** 创建时间 */
|
||||
createTime?: string
|
||||
|
@ -194,12 +194,14 @@ export interface EquipmentResp {
|
|||
inventoryBasis?: string
|
||||
/** 动态记录 */
|
||||
dynamicRecord?: string
|
||||
|
||||
/** 采购状态 */
|
||||
procurementStatus?: string
|
||||
|
||||
/** 审批状态 */
|
||||
approvalStatus?: string
|
||||
/** 收货状态 */
|
||||
receiptStatus?: string
|
||||
/** 支付状态 */
|
||||
paymentStatus?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -406,3 +408,221 @@ export interface EquipmentApprovalResp {
|
|||
/** 更新时间 */
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 收货请求参数(扩展版,包含完整设备信息)
|
||||
*/
|
||||
export interface ReceiptRequest {
|
||||
// 收货特有信息
|
||||
receiptTime: string
|
||||
receiptPerson: string
|
||||
receiptQuantity: number
|
||||
receiptRemark?: string
|
||||
appearanceCheck: string
|
||||
functionTest: string
|
||||
packageIntegrity: string
|
||||
accessoryIntegrity: string
|
||||
checkResult: 'PASS' | 'FAIL' | 'CONDITIONAL'
|
||||
checkRemark?: string
|
||||
storageLocation: string
|
||||
storageManager: string
|
||||
|
||||
// 设备基本信息(从采购数据继承)
|
||||
equipmentName?: string
|
||||
equipmentModel?: string
|
||||
equipmentType?: string
|
||||
equipmentSn?: string
|
||||
brand?: string
|
||||
specification?: string
|
||||
assetCode?: string
|
||||
|
||||
// 采购信息(从采购数据继承)
|
||||
purchaseOrder?: string
|
||||
supplierName?: string
|
||||
purchasePrice?: number
|
||||
purchaseTime?: string
|
||||
quantity?: number
|
||||
unitPrice?: number
|
||||
totalPrice?: number
|
||||
|
||||
// 入库信息
|
||||
inStockTime?: string
|
||||
physicalLocation?: string
|
||||
locationStatus?: string
|
||||
responsiblePerson?: string
|
||||
inventoryBarcode?: string
|
||||
|
||||
// 状态信息
|
||||
equipmentStatus?: string
|
||||
useStatus?: string
|
||||
healthStatus?: string
|
||||
receiptStatus?: string
|
||||
|
||||
// 其他管理信息
|
||||
depreciationMethod?: string
|
||||
depreciationYears?: number
|
||||
salvageValue?: number
|
||||
currentNetValue?: number
|
||||
|
||||
// 系统字段
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 收货详情响应
|
||||
*/
|
||||
export interface ReceiptDetail {
|
||||
/** 设备ID */
|
||||
equipmentId: string
|
||||
/** 设备名称 */
|
||||
equipmentName: string
|
||||
/** 设备类型 */
|
||||
equipmentType: string
|
||||
/** 设备型号 */
|
||||
equipmentModel: string
|
||||
/** 品牌 */
|
||||
brand: string
|
||||
/** 供应商 */
|
||||
supplierName: string
|
||||
/** 采购订单 */
|
||||
purchaseOrder: string
|
||||
/** 收货状态 */
|
||||
receiptStatus: string
|
||||
/** 收货时间 */
|
||||
receiptTime: string
|
||||
/** 收货人 */
|
||||
receiptPerson: string
|
||||
/** 收货数量 */
|
||||
receiptQuantity: number
|
||||
/** 收货备注 */
|
||||
receiptRemark?: string
|
||||
/** 外观检查 */
|
||||
appearanceCheck: string
|
||||
/** 功能测试 */
|
||||
functionTest: string
|
||||
/** 包装完整性 */
|
||||
packageIntegrity: string
|
||||
/** 配件完整性 */
|
||||
accessoryIntegrity: string
|
||||
/** 检查结果 */
|
||||
checkResult: string
|
||||
/** 检查备注 */
|
||||
checkRemark?: string
|
||||
/** 入库状态 */
|
||||
storageStatus: string
|
||||
/** 入库时间 */
|
||||
storageTime: string
|
||||
/** 入库位置 */
|
||||
storageLocation: string
|
||||
/** 库管员 */
|
||||
storageManager: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付请求参数
|
||||
*/
|
||||
export interface PaymentRequest {
|
||||
/** 支付方式 */
|
||||
paymentMethod: string
|
||||
/** 支付金额 */
|
||||
paymentAmount: number
|
||||
/** 支付时间 */
|
||||
paymentTime: string
|
||||
/** 支付人 */
|
||||
paymentPerson: string
|
||||
/** 支付备注 */
|
||||
paymentRemark?: string
|
||||
/** 发票类型 */
|
||||
invoiceType: string
|
||||
/** 发票号码 */
|
||||
invoiceNumber: string
|
||||
/** 开票日期 */
|
||||
invoiceDate: string
|
||||
/** 发票金额 */
|
||||
invoiceAmount: number
|
||||
/** 税率 */
|
||||
taxRate: number
|
||||
/** 税额 */
|
||||
taxAmount: number
|
||||
/** 不含税金额 */
|
||||
amountWithoutTax: number
|
||||
/** 合同编号 */
|
||||
contractNumber: string
|
||||
/** 合同金额 */
|
||||
contractAmount: number
|
||||
/** 签订日期 */
|
||||
contractDate: string
|
||||
/** 付款条件 */
|
||||
paymentTerms: string
|
||||
/** 付款期限 */
|
||||
paymentDeadline: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付详情响应
|
||||
*/
|
||||
export interface PaymentDetail {
|
||||
/** 设备ID */
|
||||
equipmentId: string
|
||||
/** 设备名称 */
|
||||
equipmentName: string
|
||||
/** 设备类型 */
|
||||
equipmentType: string
|
||||
/** 设备型号 */
|
||||
equipmentModel: string
|
||||
/** 品牌 */
|
||||
brand: string
|
||||
/** 供应商 */
|
||||
supplierName: string
|
||||
/** 采购订单 */
|
||||
purchaseOrder: string
|
||||
/** 采购价格 */
|
||||
purchasePrice: number
|
||||
/** 采购数量 */
|
||||
quantity: number
|
||||
/** 总金额 */
|
||||
totalPrice: number
|
||||
/** 采购日期 */
|
||||
purchaseTime: string
|
||||
/** 支付状态 */
|
||||
paymentStatus: string
|
||||
/** 支付方式 */
|
||||
paymentMethod: string
|
||||
/** 支付金额 */
|
||||
paymentAmount: number
|
||||
/** 支付时间 */
|
||||
paymentTime: string
|
||||
/** 支付人 */
|
||||
paymentPerson: string
|
||||
/** 支付备注 */
|
||||
paymentRemark?: string
|
||||
/** 发票状态 */
|
||||
invoiceStatus: string
|
||||
/** 发票类型 */
|
||||
invoiceType: string
|
||||
/** 发票号码 */
|
||||
invoiceNumber: string
|
||||
/** 开票日期 */
|
||||
invoiceDate: string
|
||||
/** 发票金额 */
|
||||
invoiceAmount: number
|
||||
/** 税率 */
|
||||
taxRate: number
|
||||
/** 税额 */
|
||||
taxAmount: number
|
||||
/** 不含税金额 */
|
||||
amountWithoutTax: number
|
||||
/** 合同编号 */
|
||||
contractNumber: string
|
||||
/** 合同状态 */
|
||||
contractStatus: string
|
||||
/** 合同金额 */
|
||||
contractAmount: number
|
||||
/** 签订日期 */
|
||||
contractDate: string
|
||||
/** 付款条件 */
|
||||
paymentTerms: string
|
||||
/** 付款期限 */
|
||||
paymentDeadline: string
|
||||
}
|
||||
|
|
|
@ -15,6 +15,12 @@ export function getProject(id: string | number) {
|
|||
return http.get<T.ProjectResp>(`${BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
/** @desc 获取项目详情(标准详情接口) */
|
||||
export function getProjectDetail(id: string | number) {
|
||||
return http.get<T.ProjectResp>(`${BASE_URL}/detail/${id}`)
|
||||
}
|
||||
|
||||
|
||||
/** @desc 新增项目 */
|
||||
export function addProject(data: any) {
|
||||
return http.post(`${BASE_URL}`, data)
|
||||
|
|
|
@ -18,6 +18,8 @@ export interface ProjectResp {
|
|||
projectCategory?: string // 项目类型/服务
|
||||
projectManagerId?: string // 项目经理ID
|
||||
projectManagerName?: string // 项目经理姓名
|
||||
projectOrigin?: string // 项目来源
|
||||
|
||||
projectStaff?: string[] // 施工人员
|
||||
startDate?: string // 开始日期
|
||||
endDate?: string // 结束日期
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
:mask-closable="true"
|
||||
:closable="true"
|
||||
:destroy-on-close="false"
|
||||
:z-index="999999"
|
||||
:z-index="1000"
|
||||
class="notification-modal"
|
||||
>
|
||||
<!-- 消息中心头部 -->
|
||||
|
@ -673,7 +673,7 @@ defineExpose({
|
|||
<style scoped lang="scss">
|
||||
.notification-center {
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
z-index: 1000;
|
||||
|
||||
.notification-trigger {
|
||||
cursor: pointer;
|
||||
|
@ -699,15 +699,15 @@ defineExpose({
|
|||
// 消息中心弹窗样式
|
||||
.notification-modal {
|
||||
:deep(.arco-modal) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-modal-mask) {
|
||||
z-index: 999998 !important;
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-modal-wrapper) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -858,37 +858,37 @@ defineExpose({
|
|||
padding: 16px;
|
||||
}
|
||||
|
||||
// 确保弹窗在最上层
|
||||
// 确保弹窗在普通层级
|
||||
:deep(.arco-modal) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-modal-mask) {
|
||||
z-index: 999998 !important;
|
||||
z-index: 999 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-modal-wrapper) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
// 针对Arco Design v2的样式
|
||||
:deep(.arco-overlay) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-overlay-container) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
// 确保消息中心弹窗不被其他元素遮挡
|
||||
// 确保消息中心弹窗在普通层级
|
||||
:deep(.arco-modal) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
// 强制设置最高优先级
|
||||
// 强制设置普通优先级
|
||||
:deep(.arco-modal-wrapper) {
|
||||
z-index: 999999 !important;
|
||||
z-index: 1000 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1199,46 +1199,141 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||
],
|
||||
},
|
||||
{
|
||||
path: '/chat-platform',
|
||||
name: 'ChatPlatform',
|
||||
path: '/image-detection',
|
||||
name: 'ImageDetection',
|
||||
component: Layout,
|
||||
redirect: '/chat-platform/options',
|
||||
meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
|
||||
redirect: '/Image-detection/tower-monitoring/clearance-monitoring',
|
||||
meta: {
|
||||
title: '图像检测',
|
||||
icon: 'monitor',
|
||||
hidden: false,
|
||||
sort: 6.5,
|
||||
},
|
||||
|
||||
children: [
|
||||
// {
|
||||
// path: '/chat-platform/options',
|
||||
// name: 'ChatOptions',
|
||||
// component: () => import('@/views/default/redirect/index.vue'), // 临时使用一个组件,实际开发中需要替换
|
||||
// meta: {
|
||||
// title: '二级选项1',
|
||||
// icon: 'setting',
|
||||
// hidden: false
|
||||
// }
|
||||
// }
|
||||
{
|
||||
path: '/image-detection/image-analysis',
|
||||
name: 'ImageAnalysis',
|
||||
component: () => import('@/components/ParentView/index.vue'),
|
||||
meta: {
|
||||
title: '检查图像分析',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
},
|
||||
redirect: '/image-detection/image-analysis/defect-detection',
|
||||
children: [
|
||||
{path: '/image-detection/image-analysis/defect-detection',
|
||||
name: 'DefectDetection',
|
||||
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/defect-algorithm/index.vue'),
|
||||
meta: {
|
||||
title: '缺陷检测',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
}
|
||||
},
|
||||
{path: '/image-detection/image-analysis/defect-edit',
|
||||
name: 'DefectEdit',
|
||||
component: () => import('@/components/ParentView/index.vue'),
|
||||
meta: {
|
||||
title: '缺陷编辑',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
}
|
||||
},
|
||||
{path: '/image-detection/image-analysis/generate-reports',
|
||||
name: 'GenerateReports',
|
||||
component: () => import('@/views/project-operation-platform/data-processing/report-generation/index.vue'),
|
||||
meta: {
|
||||
title: '生成报告',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
}
|
||||
},
|
||||
{path: '/image-detection/image-analysis/defect-base',
|
||||
name: 'DefectBase',
|
||||
component: () => import('@/views/project-operation-platform/data-processing/standard-info/index.vue'),
|
||||
meta: {
|
||||
title: '缺陷标准数据信息库',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tower-monitoring',
|
||||
name: 'TowerMonitoring',
|
||||
component: () => import('@/components/ParentView/index.vue'),
|
||||
redirect: '/tower-monitoring/clearance-monitoring',
|
||||
meta: {
|
||||
title: '音视频检测',
|
||||
icon: 'monitor',
|
||||
hidden: false,
|
||||
sort: 6.5,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/tower-monitoring/clearance-monitoring',
|
||||
name: 'ClearanceMonitoring',
|
||||
component: () => import('@/views/tower-monitoring/deformation.vue'),
|
||||
meta: {
|
||||
title: '净空监测',
|
||||
icon: 'fullscreen',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tower-monitoring/deformation-monitoring',
|
||||
name: 'DeformationMonitoring',
|
||||
component: () => import('@/views/tower-monitoring/clearance.vue'),
|
||||
meta: {
|
||||
title: '形变监测',
|
||||
icon: 'line-chart',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tower-monitoring/whistle-monitoring',
|
||||
name: 'WhistleMonitoring',
|
||||
component: () => import('@/views/tower-monitoring/whistle.vue'),
|
||||
meta: {
|
||||
title: '哨声监测',
|
||||
icon: 'sound',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tower-monitoring/vibration-monitoring',
|
||||
name: 'VibrationMonitoring',
|
||||
component: () => import('@/views/tower-monitoring/vibration.vue'),
|
||||
meta: {
|
||||
title: '振动监测',
|
||||
icon: 'shake',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/image-detection/reporting-center',
|
||||
name: 'ReportingCenter',
|
||||
component: () => import('@/views/tower-monitoring/vibration.vue'),
|
||||
meta: {
|
||||
title: '报告中心',
|
||||
icon: 'shake',
|
||||
hidden: false,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// {
|
||||
// path: '/user/profile',
|
||||
// name: 'UserProfile',
|
||||
// path: '/chat-platform',
|
||||
// name: 'ChatPlatform',
|
||||
// component: Layout,
|
||||
// redirect: '/user/profile',
|
||||
// meta: {
|
||||
// title: '个人中心',
|
||||
// icon: 'user',
|
||||
// hidden: false,
|
||||
// sort: 100,
|
||||
// },
|
||||
// redirect: '/chat-platform/options',
|
||||
// meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
|
||||
// children: [
|
||||
// {
|
||||
// path: '/user/profile',
|
||||
// name: 'UsersProfile',
|
||||
// component: () => import('@/views/user/profile/index.vue'),
|
||||
// meta: {
|
||||
// title: '个人中心',
|
||||
// icon: 'user',
|
||||
// hidden: false,
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
|
@ -1356,6 +1451,16 @@ export const systemRoutes: RouteRecordRaw[] = [
|
|||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/system-resource/device-management/inventory',
|
||||
name: 'DeviceInventory',
|
||||
component: () => import('@/views/system-resource/device-management/inventory.vue'),
|
||||
meta: {
|
||||
title: '设备盘库',
|
||||
icon: 'inbox',
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/system-resource/device-management/online',
|
||||
name: 'SystemResourceDeviceOnline',
|
||||
|
|
|
@ -226,6 +226,20 @@ const storeSetup = () => {
|
|||
{
|
||||
id: 2013,
|
||||
parentId: 2010,
|
||||
title: '设备盘库',
|
||||
type: 2,
|
||||
path: '/asset-management/device-management/inventory',
|
||||
name: 'DeviceInventory',
|
||||
component: 'system-resource/device-management/inventory',
|
||||
icon: 'inbox',
|
||||
isExternal: false,
|
||||
isCache: false,
|
||||
isHidden: false,
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
id: 2014,
|
||||
parentId: 2010,
|
||||
title: '审批台',
|
||||
type: 2,
|
||||
path: '/asset-management/device-management/approval',
|
||||
|
@ -235,10 +249,10 @@ const storeSetup = () => {
|
|||
isExternal: false,
|
||||
isCache: false,
|
||||
isHidden: false,
|
||||
sort: 3,
|
||||
sort: 4,
|
||||
},
|
||||
{
|
||||
id: 2014,
|
||||
id: 2015,
|
||||
parentId: 2010,
|
||||
title: '在线管理',
|
||||
type: 1,
|
||||
|
@ -250,11 +264,11 @@ const storeSetup = () => {
|
|||
isExternal: false,
|
||||
isCache: false,
|
||||
isHidden: false,
|
||||
sort: 4,
|
||||
sort: 5,
|
||||
children: [
|
||||
{
|
||||
id: 20141,
|
||||
parentId: 2014,
|
||||
id: 20151,
|
||||
parentId: 2015,
|
||||
title: '无人机',
|
||||
type: 2,
|
||||
path: '/asset-management/device-management/online/drone',
|
||||
|
@ -267,8 +281,8 @@ const storeSetup = () => {
|
|||
sort: 1,
|
||||
},
|
||||
{
|
||||
id: 20142,
|
||||
parentId: 2014,
|
||||
id: 20152,
|
||||
parentId: 2015,
|
||||
title: '机巢',
|
||||
type: 2,
|
||||
path: '/asset-management/device-management/online/nest',
|
||||
|
@ -281,8 +295,8 @@ const storeSetup = () => {
|
|||
sort: 2,
|
||||
},
|
||||
{
|
||||
id: 20143,
|
||||
parentId: 2014,
|
||||
id: 20153,
|
||||
parentId: 2015,
|
||||
title: '其他智能终端',
|
||||
type: 2,
|
||||
path: '/asset-management/device-management/online/smart-terminal',
|
||||
|
@ -297,7 +311,7 @@ const storeSetup = () => {
|
|||
],
|
||||
},
|
||||
{
|
||||
id: 2015,
|
||||
id: 2016,
|
||||
parentId: 2010,
|
||||
title: '设备详情',
|
||||
type: 2,
|
||||
|
@ -308,7 +322,7 @@ const storeSetup = () => {
|
|||
isExternal: false,
|
||||
isCache: false,
|
||||
isHidden: true,
|
||||
sort: 5,
|
||||
sort: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -7,70 +7,7 @@ export {}
|
|||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default']
|
||||
ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default']
|
||||
Avatar: typeof import('./../components/Avatar/index.vue')['default']
|
||||
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
|
||||
CellCopy: typeof import('./../components/CellCopy/index.vue')['default']
|
||||
Chart: typeof import('./../components/Chart/index.vue')['default']
|
||||
CircularProgress: typeof import('./../components/CircularProgress/index.vue')['default']
|
||||
ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default']
|
||||
CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default']
|
||||
CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default']
|
||||
DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default']
|
||||
DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default']
|
||||
FilePreview: typeof import('./../components/FilePreview/index.vue')['default']
|
||||
GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default']
|
||||
GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default']
|
||||
GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default']
|
||||
GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default']
|
||||
GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default']
|
||||
GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default']
|
||||
GiDot: typeof import('./../components/GiDot/index.tsx')['default']
|
||||
GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default']
|
||||
GiFooter: typeof import('./../components/GiFooter/index.vue')['default']
|
||||
GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default']
|
||||
GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default']
|
||||
GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default']
|
||||
GiIframe: typeof import('./../components/GiIframe/index.vue')['default']
|
||||
GiOption: typeof import('./../components/GiOption/index.vue')['default']
|
||||
GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default']
|
||||
GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default']
|
||||
GiSpace: typeof import('./../components/GiSpace/index.vue')['default']
|
||||
GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default']
|
||||
GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default']
|
||||
GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default']
|
||||
GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default']
|
||||
GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default']
|
||||
GiTag: typeof import('./../components/GiTag/index.tsx')['default']
|
||||
GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default']
|
||||
HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default']
|
||||
Icon403: typeof import('./../components/icons/Icon403.vue')['default']
|
||||
Icon404: typeof import('./../components/icons/Icon404.vue')['default']
|
||||
Icon500: typeof import('./../components/icons/Icon500.vue')['default']
|
||||
IconBorders: typeof import('./../components/icons/IconBorders.vue')['default']
|
||||
IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default']
|
||||
IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default']
|
||||
IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default']
|
||||
ImageImport: typeof import('./../components/ImageImport/index.vue')['default']
|
||||
ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default']
|
||||
IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default']
|
||||
JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default']
|
||||
MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default']
|
||||
MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default']
|
||||
NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default']
|
||||
ParentView: typeof import('./../components/ParentView/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
|
||||
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
|
||||
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
|
||||
TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default']
|
||||
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
|
||||
Verify: typeof import('./../components/Verify/index.vue')['default']
|
||||
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
|
||||
VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default']
|
||||
WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default']
|
||||
YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,10 +143,12 @@
|
|||
|
||||
<a-layout-content class="file-content">
|
||||
<a-card :bordered="false" class="file-card">
|
||||
<a-descriptions :title="`文件列表 (${fileList.length})`" v-if="currentFolderId" />
|
||||
|
||||
<!-- 文件搜索功能 -->
|
||||
<div v-if="currentFolderId" class="file-search-container">
|
||||
<!-- 文件列表标题和搜索框在同一行 -->
|
||||
<div v-if="currentFolderId" class="file-header-container">
|
||||
<div class="file-title">
|
||||
<span class="file-list-title">文件列表 ({{ fileList.length }})</span>
|
||||
</div>
|
||||
<div class="file-search-container">
|
||||
<a-input-search
|
||||
v-model="fileSearchKeyword"
|
||||
placeholder="搜索文件名..."
|
||||
|
@ -157,6 +159,7 @@
|
|||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider size="small" v-if="currentFolderId" />
|
||||
|
||||
|
@ -336,7 +339,7 @@
|
|||
</div>
|
||||
|
||||
<!-- 文件分页 -->
|
||||
<div v-if="currentFolderId && !loading && totalFiles > 0" class="file-pagination">
|
||||
<div v-if="currentFolderId && !loading && totalFiles > 0" class="pagination-container">
|
||||
<a-pagination
|
||||
:total="totalFiles"
|
||||
:current="fileCurrentPage"
|
||||
|
@ -345,6 +348,8 @@
|
|||
:show-page-size="true"
|
||||
:page-size-options="[10, 20, 50, 100]"
|
||||
:show-jumper="true"
|
||||
:hide-on-single-page="false"
|
||||
size="default"
|
||||
@change="handleFilePageChange"
|
||||
@page-size-change="handleFilePageSizeChange"
|
||||
/>
|
||||
|
@ -2453,8 +2458,6 @@ onMounted(() => {
|
|||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.folder-sidebar {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
|
@ -2629,6 +2632,7 @@ onMounted(() => {
|
|||
background: var(--color-bg-1);
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 120px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-card {
|
||||
|
@ -2640,6 +2644,7 @@ onMounted(() => {
|
|||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding-bottom: 80px; /* 为分页器留出空间 */
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
|
@ -2654,7 +2659,7 @@ onMounted(() => {
|
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 0;
|
||||
min-height: 300px;
|
||||
max-height: calc(100vh - 300px);
|
||||
max-height: calc(100vh - 380px); /* 调整高度为分页器留出空间 */
|
||||
}
|
||||
|
||||
/* 表头行样式 */
|
||||
|
@ -3100,14 +3105,65 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
/* 分页样式 */
|
||||
.pagination-container, .file-pagination {
|
||||
margin-top: 16px;
|
||||
text-align: right;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.file-pagination {
|
||||
.pagination-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-bg-1);
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-top: 0;
|
||||
|
||||
.arco-pagination {
|
||||
margin: 0;
|
||||
|
||||
.arco-pagination-item {
|
||||
border-radius: 6px;
|
||||
margin: 0 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.arco-pagination-item-active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-pagination-prev,
|
||||
.arco-pagination-next {
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.arco-pagination-size-changer {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.arco-pagination-jumper {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.arco-pagination-total {
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -3530,9 +3586,29 @@ onMounted(() => {
|
|||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
/* 文件头部容器样式 */
|
||||
.file-header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.file-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-list-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 文件搜索样式 */
|
||||
.file-search-container {
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
@ -3601,55 +3677,7 @@ onMounted(() => {
|
|||
border-top-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 文件分页样式 */
|
||||
.file-pagination {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: var(--color-bg-1);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
|
||||
.arco-pagination {
|
||||
.arco-pagination-total {
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.arco-pagination-item {
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.arco-pagination-item-active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-pagination-prev,
|
||||
.arco-pagination-next {
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 树形文件夹结构 */
|
||||
.folder-tree-container {
|
||||
|
|
|
@ -73,18 +73,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="props" tap="形变" title="形变原数据">
|
||||
<div class="tab-content">
|
||||
<raw-data>
|
||||
|
||||
</raw-data>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 文件预览模态框(待重新设计) -->
|
||||
<!-- <PreviewModal ref="previewModal" /> -->
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
|
@ -107,8 +97,6 @@ import {
|
|||
} from '@/apis/industrial-image'
|
||||
import DeformationTap from './components/DeformationTap.vue'
|
||||
|
||||
// 预览弹窗引用(待重新设计)
|
||||
// const previewModal = ref()
|
||||
// 活动选项卡
|
||||
const activeTab = ref('image')
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
<template #icon><icon-arrow-left /></template>
|
||||
</a-button>
|
||||
<h2 class="ml-2">{{ projectTitle }}</h2>
|
||||
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{
|
||||
projectData.status
|
||||
}}</a-tag>
|
||||
<a-tag class="ml-2" :color="getStatusColor(projectData.statusLabel ?? projectData.status)" v-if="projectData.status !== undefined && projectData.status !== null">
|
||||
{{ projectData.statusLabel ?? projectData.status }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<a-button v-permission="['project:update']" type="primary" class="mr-2" @click="editProject">
|
||||
|
@ -198,8 +198,8 @@
|
|||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { getProject, deleteProject } from '@/apis/project'
|
||||
import { addTask, addTaskGroup, listTask, updateTaskProgress } from '@/apis/project/task'
|
||||
import { getProjectDetail, deleteProject } from '@/apis/project'
|
||||
import { addTask, addTaskGroup, updateTaskProgress } from '@/apis/project/task'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
defineOptions({ name: 'ProjectDetail' })
|
||||
|
@ -257,10 +257,15 @@ const projectTitle = computed(() => {
|
|||
|
||||
const projectInfos = computed(() => [
|
||||
{ label: '项目编号', value: projectData.value?.projectCode },
|
||||
{ label: '项目负责人', value: projectData.value?.projectManager },
|
||||
{ label: '参与人', value: projectData.value?.projectStaff?.join(', ') },
|
||||
{ label: '项目周期', value: projectData.value?.projectPeriod ? `${projectData.value.projectPeriod[0]} 至 ${projectData.value.projectPeriod[1]}` : '' },
|
||||
{ label: '客户', value: projectData.value?.commissionUnit },
|
||||
{ label: '项目负责人', value: projectData.value?.projectManagerName || projectData.value?.projectManager },
|
||||
{ label: '项目来源', value: projectData.value?.projectOrigin },
|
||||
{ label: '风场名称', value: projectData.value?.farmName },
|
||||
{ label: '风场地址', value: projectData.value?.farmAddress },
|
||||
{ label: '开始时间', value: projectData.value?.startDate },
|
||||
{ label: '结束时间', value: projectData.value?.endDate },
|
||||
{ label: '项目规模', value: projectData.value?.scale },
|
||||
{ label: '状态', value: (statusMap as any)[Number(projectData.value?.status)]?.label || projectData.value?.statusLabel },
|
||||
{ label: '客户', value: projectData.value?.client },
|
||||
{ label: '备注', value: projectData.value?.projectIntro || '无' }
|
||||
])
|
||||
|
||||
|
@ -284,6 +289,15 @@ const taskDetailInfos = computed(() => {
|
|||
{ label: '状态', value: currentTask.value.status },
|
||||
{ label: '描述', value: currentTask.value.description || '无' }
|
||||
]
|
||||
|
||||
const statusMap: Record<number, { label: string; color: string }> = {
|
||||
0: { label: '待施工', color: 'gray' },
|
||||
1: { label: '施工中', color: 'blue' },
|
||||
2: { label: '已完工', color: 'green' },
|
||||
3: { label: '已审核', color: 'orange' },
|
||||
4: { label: '已验收', color: 'arcoblue' },
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
|
@ -322,8 +336,14 @@ const formatDate = (date: string) => {
|
|||
const fetchProjectData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProject(projectId.value)
|
||||
projectData.value = res.data
|
||||
const res = await getProjectDetail(projectId.value)
|
||||
const detail = (res as any).data || res
|
||||
// 如果status是数字,补充statusLabel用于页面展示
|
||||
if (typeof detail.status === 'number' && !detail.statusLabel) {
|
||||
const mapper = (statusMap as any)[detail.status]
|
||||
if (mapper) detail.statusLabel = mapper.label
|
||||
}
|
||||
projectData.value = detail
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Message.error('获取项目详情失败')
|
||||
|
@ -332,34 +352,39 @@ const fetchProjectData = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const fetchTaskData = async () => {
|
||||
try {
|
||||
const res = await listTask({
|
||||
projectId: projectId.value,
|
||||
page: 1,
|
||||
size: 100
|
||||
})
|
||||
// 若任务未返回状态,依据计划时间简单推断状态
|
||||
const inferTaskStatus = (task: any): string => {
|
||||
if (task.status) return task.status
|
||||
const now = dayjs()
|
||||
const start = task.planStartDate ? dayjs(task.planStartDate) : null
|
||||
const end = task.planEndDate ? dayjs(task.planEndDate) : null
|
||||
if (end && end.isBefore(now)) return '已完成'
|
||||
if (start && start.isAfter(now)) return '计划中'
|
||||
if (start && (!end || end.isAfter(now))) return '正在做'
|
||||
return '其他'
|
||||
}
|
||||
|
||||
|
||||
const fetchTaskData = () => {
|
||||
// 使用详情接口返回的任务列表
|
||||
const detail = projectData.value || {}
|
||||
const tasks = (detail.tasks || []) as any[]
|
||||
|
||||
// 重置任务列表
|
||||
taskColumns.value.forEach(column => {
|
||||
column.tasks = []
|
||||
})
|
||||
|
||||
const tasks = res.data?.list || []
|
||||
|
||||
// 分配任务到对应的列
|
||||
// 分配任务到对应的列(按状态或推断状态)
|
||||
tasks.forEach((task: any) => {
|
||||
const column = taskColumns.value.find(col => col.status === task.status)
|
||||
const st = inferTaskStatus(task)
|
||||
const column = taskColumns.value.find(col => col.status === st)
|
||||
if (column) {
|
||||
column.tasks.push(task)
|
||||
} else {
|
||||
taskColumns.value.find(col => col.status === '其他')?.tasks.push(task)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Message.error('获取任务数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
|
@ -491,8 +516,7 @@ const submitProgressUpdate = async () => {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjectData()
|
||||
fetchTaskData()
|
||||
fetchProjectData().then(() => fetchTaskData())
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -12,9 +12,11 @@
|
|||
-->
|
||||
<template>
|
||||
<GiPageLayout>
|
||||
<GiTable row-key="id" :data="dataList" :columns="tableColumns" :loading="loading"
|
||||
<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">
|
||||
@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>
|
||||
|
@ -71,10 +73,14 @@
|
|||
</GiTable>
|
||||
|
||||
<!-- 新增/编辑项目弹窗 -->
|
||||
<a-modal v-model:visible="addModalVisible" :title="modalTitle" @cancel="resetForm"
|
||||
:ok-button-props="{ loading: submitLoading }" @ok="handleSubmit" width="800px" modal-class="project-form-modal">
|
||||
<a-form ref="formRef" :model="form" :rules="formRules" layout="vertical"
|
||||
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }">
|
||||
<a-modal
|
||||
v-model:visible="addModalVisible" :title="modalTitle" :ok-button-props="{ loading: submitLoading }"
|
||||
width="800px" modal-class="project-form-modal" @cancel="resetForm" @ok="handleSubmit"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef" :model="form" :rules="formRules" layout="vertical"
|
||||
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<a-divider orientation="left">基本信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
|
@ -88,10 +94,12 @@
|
|||
<a-input v-model="form.farmAddress" placeholder="请输入地址" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col><a-button size="mini" @click="() => { Message.info(`待开发`) }">
|
||||
<a-col>
|
||||
<a-button size="mini" @click="() => { Message.info(`待开发`) }">
|
||||
<template #icon><icon-location /></template>
|
||||
地图选点
|
||||
</a-button></a-col>
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
|
@ -108,7 +116,7 @@
|
|||
<a-col :span="12">
|
||||
<a-form-item field="inspectionUnit" label="业主">
|
||||
<a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (form.farmName = val)" />
|
||||
<!--风场名称同步业主 -->
|
||||
<!-- 风场名称同步业主 -->
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
|
@ -145,10 +153,133 @@
|
|||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-form-item field="projectContent" label="项目内容">
|
||||
<a-textarea v-model="form.coverUrl" placeholder="请输入项目内容" :rows="4" />
|
||||
<a-col :span="12">
|
||||
<a-form-item field="projectOrigin" label="项目来源" :rules="[{ required: true, message: '请输入项目来源' }]">
|
||||
<a-input v-model="form.projectOrigin" placeholder="请输入项目来源" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider orientation="left">任务设置</a-divider>
|
||||
<div class="mb-2">
|
||||
<a-button type="dashed" size="small" @click="addTask">
|
||||
<template #icon><icon-plus /></template>
|
||||
新增任务
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="form.tasks.length === 0" class="text-gray-500 mb-2">暂无任务,请点击“新增任务”。</div>
|
||||
<a-space direction="vertical" fill>
|
||||
<a-card v-for="(task, tIndex) in form.tasks" :key="tIndex" size="small">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>任务 {{ tIndex + 1 }}</span>
|
||||
<a-space>
|
||||
<a-button size="mini" @click="addSubtask(tIndex)">新增子任务</a-button>
|
||||
<a-button size="mini" status="danger" @click="removeTask(tIndex)">删除</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.taskName`" label="任务名称" required>
|
||||
<a-input v-model="task.taskName" placeholder="请输入任务名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item :field="`tasks.${tIndex}.taskCode`" label="任务编号">
|
||||
<a-input v-model="task.taskCode" placeholder="编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item :field="`tasks.${tIndex}.mainUserId`" label="负责人">
|
||||
<a-select v-model="task.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
||||
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item :field="`tasks.${tIndex}.scales`" label="工量">
|
||||
<a-input-number v-model="task.scales" :min="0" :max="9999" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.planStartDate`" label="计划开始">
|
||||
<a-date-picker v-model="task.planStartDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.planEndDate`" label="计划结束">
|
||||
<a-date-picker v-model="task.planEndDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.taskGroupId`" label="任务组">
|
||||
<a-input-number v-model="task.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 子任务 -->
|
||||
<div v-if="task.children && task.children.length">
|
||||
<a-divider orientation="left">子任务</a-divider>
|
||||
<a-card
|
||||
v-for="(sub, sIndex) in task.children"
|
||||
:key="sIndex"
|
||||
size="small"
|
||||
class="mb-2"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>子任务 {{ tIndex + 1 }}-{{ sIndex + 1 }}</span>
|
||||
<a-button size="mini" status="danger" @click="removeSubtask(tIndex, sIndex)">删除</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskName`" label="任务名称" required>
|
||||
<a-input v-model="sub.taskName" placeholder="请输入任务名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskCode`" label="任务编号">
|
||||
<a-input v-model="sub.taskCode" placeholder="编号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.mainUserId`" label="负责人">
|
||||
<a-select v-model="sub.mainUserId" placeholder="选择负责人" :loading="userLoading">
|
||||
<a-option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="4">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.scales`" label="工量">
|
||||
<a-input-number v-model="sub.scales" :min="0" :max="9999" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planStartDate`" label="计划开始">
|
||||
<a-date-picker v-model="sub.planStartDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.planEndDate`" label="计划结束">
|
||||
<a-date-picker v-model="sub.planEndDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item :field="`tasks.${tIndex}.children.${sIndex}.taskGroupId`" label="任务组">
|
||||
<a-input-number v-model="sub.taskGroupId" :min="0" placeholder="可选" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-space>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="status" label="项目状态">
|
||||
|
@ -198,7 +329,6 @@
|
|||
</a-col>
|
||||
</a-row>
|
||||
<a-divider orientation="middle">地图</a-divider>
|
||||
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
|
@ -221,18 +351,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } 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 type { TableColumnData } from '@arco-design/web-vue'
|
||||
import TurbineGrid from './TurbineGrid.vue'
|
||||
import { addProject, deleteProject, exportProject, importProject, listProject, updateProject, getProjectDetail } from '@/apis/project'
|
||||
import { isMobile } from '@/utils'
|
||||
import has from '@/utils/has'
|
||||
import http from '@/utils/http'
|
||||
import type { ColumnItem } from '@/components/GiForm'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import type { ProjectResp, ProjectPageQuery } from '@/apis/project/type'
|
||||
import * as T from '@/apis/project/type'
|
||||
import TurbineGrid from './TurbineGrid.vue'
|
||||
import type { ProjectPageQuery } from '@/apis/project/type'
|
||||
import type * as T from '@/apis/project/type'
|
||||
|
||||
defineOptions({ name: 'ProjectManagement' })
|
||||
|
||||
// 项目状态常量定义 (API返回数字类型)
|
||||
|
@ -246,28 +376,28 @@ const PROJECT_STATUS = {
|
|||
const PROJECT_STATUS_MAP = {
|
||||
0: '待施工',
|
||||
1: '施工中',
|
||||
2: '已完成'
|
||||
2: '已完成',
|
||||
} as const
|
||||
|
||||
// 项目状态选项
|
||||
const PROJECT_STATUS_OPTIONS = [
|
||||
{ label: '待施工', value: 0 },
|
||||
{ label: '施工中', value: 1 },
|
||||
{ label: '已完成', value: 2 }
|
||||
{ label: '已完成', value: 2 },
|
||||
]
|
||||
|
||||
// 项目类别常量定义
|
||||
const PROJECT_CATEGORY = {
|
||||
EXTERNAL_WORK: '外部工作',
|
||||
INTERNAL_PROJECT: '内部项目',
|
||||
TECHNICAL_SERVICE: '技术服务'
|
||||
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 }
|
||||
{ label: PROJECT_CATEGORY.TECHNICAL_SERVICE, value: PROJECT_CATEGORY.TECHNICAL_SERVICE },
|
||||
]
|
||||
|
||||
const router = useRouter()
|
||||
|
@ -280,9 +410,9 @@ const currentId = ref<string | null>(null)
|
|||
const fileList = ref([])
|
||||
const dataList = ref<T.ProjectResp[]>([])
|
||||
const userLoading = ref(false)
|
||||
const userOptions = ref<{ label: string; value: string }[]>([])
|
||||
const userOptions = ref<{ label: string, value: string }[]>([])
|
||||
|
||||
let searchForm = reactive<Partial<ProjectPageQuery>>({
|
||||
const searchForm = reactive<Partial<ProjectPageQuery>>({
|
||||
projectName: '',
|
||||
status: undefined,
|
||||
fieldName: '', // 保持使用fieldName,因为API类型定义中使用的是这个字段名
|
||||
|
@ -331,17 +461,37 @@ const form = reactive({
|
|||
inspectionPhone: '', // 业主单位联系电话
|
||||
farmName: '', // 风场名称 现在等同业主单位
|
||||
farmAddress: '', // 风场地址 项目地址
|
||||
projectOrigin: '', // 项目来源
|
||||
scale: '', // 项目规模 风机数量
|
||||
turbineModel: '', // 风机型号
|
||||
status: '', // 状态:0待施工,1施工中,2已完工,3已审核,4已验收
|
||||
startDate: '', // 开始时间
|
||||
endDate: '', // 结束时间
|
||||
coverUrl: '', // 项目封面 现在填的是项目内容
|
||||
// coverUrl: '', // 项目封面 现在改为任务,不再使用
|
||||
constructionTeamLeaderId: '', // 施工组长id
|
||||
constructorIds: '', // 施工人员id
|
||||
qualityOfficerId: '', // 质量员id
|
||||
auditorId: '', // 安全员id
|
||||
turbineList: [] as { id: number; turbineNo: string; lat?: number; lng?: number; status: 0 | 1 | 2 }[], //风机组
|
||||
// 任务集合(支持子任务)
|
||||
tasks: [] as Array<{
|
||||
taskName: string
|
||||
taskCode?: string
|
||||
mainUserId?: string | number
|
||||
planStartDate?: string
|
||||
planEndDate?: string
|
||||
scales?: number
|
||||
taskGroupId?: number | string
|
||||
children?: Array<{
|
||||
taskName: string
|
||||
taskCode?: string
|
||||
mainUserId?: string | number
|
||||
planStartDate?: string
|
||||
planEndDate?: string
|
||||
scales?: number
|
||||
taskGroupId?: number | string
|
||||
}>
|
||||
}>,
|
||||
turbineList: [] as { id: number, turbineNo: string, lat?: number, lng?: number, status: 0 | 1 | 2 }[], // 风机组
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
|
@ -350,7 +500,7 @@ const pagination = reactive({
|
|||
total: 0,
|
||||
showTotal: true,
|
||||
showJumper: true,
|
||||
showPageSize: true
|
||||
showPageSize: true,
|
||||
})
|
||||
const openMapModal = (item: any) => {
|
||||
Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`)
|
||||
|
@ -384,70 +534,70 @@ const tableColumns = ref<TableColumnData[]>([
|
|||
slotName: 'fieldInfo',
|
||||
minWidth: 180,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
align: 'center',
|
||||
width: 100
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '委托单位',
|
||||
dataIndex: 'commissionUnit',
|
||||
minWidth: 140,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '委托单位联系人/电话',
|
||||
slotName: 'commissionInfo',
|
||||
minWidth: 160,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '业主',
|
||||
dataIndex: 'inspectionUnit',
|
||||
minWidth: 140,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '业主联系人/电话',
|
||||
slotName: 'inspectionInfo',
|
||||
minWidth: 160,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '项目规模',
|
||||
dataIndex: 'projectScale',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '机组型号',
|
||||
dataIndex: 'orgNumber',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '项目经理/施工人员',
|
||||
slotName: 'projectManager',
|
||||
minWidth: 160,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '项目周期',
|
||||
slotName: 'projectPeriod',
|
||||
minWidth: 180,
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
@ -492,7 +642,7 @@ const fetchData = async () => {
|
|||
const params: ProjectPageQuery = {
|
||||
...searchForm,
|
||||
page: pagination.current,
|
||||
size: pagination.pageSize
|
||||
size: pagination.pageSize,
|
||||
}
|
||||
|
||||
const res = await listProject(params)
|
||||
|
@ -516,9 +666,9 @@ const fetchData = async () => {
|
|||
projectManager: item.projectManagerName,
|
||||
projectScale: item.scale,
|
||||
// 处理项目周期
|
||||
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : []
|
||||
};
|
||||
return mappedItem;
|
||||
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : [],
|
||||
}
|
||||
return mappedItem
|
||||
})
|
||||
|
||||
// 由于API没有返回total,使用当前数据长度
|
||||
|
@ -587,16 +737,18 @@ const resetForm = () => {
|
|||
inspectionPhone: '', // 检查单位联系电话
|
||||
farmName: '', // 风场名称
|
||||
farmAddress: '', // 风场地址
|
||||
projectOrigin: '', // 项目来源(必填)
|
||||
scale: '', // 项目规模
|
||||
turbineModel: '', // 风机型号
|
||||
status: 0, // 状态:0待施工,1施工中,2已完工,3已审核,4已验收
|
||||
startDate: '', // 开始时间
|
||||
endDate: '', // 结束时间
|
||||
coverUrl: '', // 项目封面
|
||||
// coverUrl: '', // 项目封面(已废弃)
|
||||
constructionTeamLeaderId: '', // 施工组长id
|
||||
constructorIds: '', // 施工人员id
|
||||
qualityOfficerId: '', // 质量员id
|
||||
auditorId: '' // 安全员id
|
||||
auditorId: '', // 安全员id
|
||||
tasks: [],
|
||||
})
|
||||
|
||||
isEdit.value = false
|
||||
|
@ -607,21 +759,49 @@ const openAddModal = () => {
|
|||
resetForm()
|
||||
addModalVisible.value = true
|
||||
}
|
||||
// 任务增删改(仅在新增/编辑弹窗内部使用)
|
||||
const addTask = () => {
|
||||
;(form.tasks as any[]).push({ taskName: '', taskCode: '', mainUserId: undefined, planStartDate: '', planEndDate: '', scales: undefined, taskGroupId: undefined, children: [] })
|
||||
}
|
||||
const removeTask = (index: number) => {
|
||||
;(form.tasks as any[]).splice(index, 1)
|
||||
}
|
||||
const addSubtask = (parentIndex: number) => {
|
||||
const list = (form.tasks as any[])
|
||||
if (!list[parentIndex].children) list[parentIndex].children = []
|
||||
list[parentIndex].children!.push({ taskName: '', taskCode: '', mainUserId: undefined, planStartDate: '', planEndDate: '', scales: undefined, taskGroupId: undefined })
|
||||
}
|
||||
const removeSubtask = (parentIndex: number, index: number) => {
|
||||
const list = (form.tasks as any[])
|
||||
list[parentIndex].children!.splice(index, 1)
|
||||
}
|
||||
|
||||
|
||||
const openEditModal = (record: T.ProjectResp) => {
|
||||
isEdit.value = true
|
||||
currentId.value = record.id || record.projectId || null
|
||||
|
||||
// 重置表单
|
||||
Object.keys(form).forEach(key => {
|
||||
// @ts-ignore
|
||||
form[key] = ''
|
||||
})
|
||||
|
||||
// 若需要最新详情数据,调用标准详情接口回填
|
||||
// 可避免列表接口缺少的字段在编辑时丢失
|
||||
// 注意:后端若响应结构为 { code, data, msg },这里取 res.data
|
||||
// await 的使用需要在该函数标记为 async,这里暂保留同步,如需启用请将函数改为 async 并解开注释
|
||||
// try {
|
||||
// const res = await getProjectDetail(currentId.value as any)
|
||||
// const detail = (res as any).data || res
|
||||
// Object.assign(form, {
|
||||
// ...form,
|
||||
// ...detail,
|
||||
// })
|
||||
// } catch (e) { console.error('获取项目详情失败', e) }
|
||||
|
||||
// 重置表单为默认(确保 tasks、turbineList 等为数组初始值)
|
||||
resetForm()
|
||||
|
||||
// 填充表单数据
|
||||
Object.keys(form).forEach(key => {
|
||||
Object.keys(form).forEach((key) => {
|
||||
if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
|
||||
// @ts-ignore - 这里需要处理类型转换
|
||||
// @ts-expect-error - 这里需要处理类型转换
|
||||
form[key] = record[key as keyof T.ProjectResp]
|
||||
}
|
||||
})
|
||||
|
@ -645,6 +825,7 @@ const openEditModal = (record: T.ProjectResp) => {
|
|||
// 添加表单验证规则
|
||||
const formRules = {
|
||||
projectName: [{ required: true, message: '请输入项目名称' }],
|
||||
projectOrigin: [{ required: true, message: '请输入项目来源' }],
|
||||
}
|
||||
|
||||
// 添加提交加载状态
|
||||
|
@ -660,15 +841,70 @@ const handleSubmit = async () => {
|
|||
await formRef.value.validate()
|
||||
|
||||
// 准备提交的数据
|
||||
const normalizeDate = (d: any) => (d ? (typeof d === 'string' ? d : new Date(d).toISOString().split('T')[0]) : '')
|
||||
|
||||
// 任务结构转换(只转换日期,其余保持)
|
||||
const mapTasks = (tasks: any[]) =>
|
||||
(tasks || []).map(t => ({
|
||||
...t,
|
||||
planStartDate: normalizeDate(t.planStartDate),
|
||||
planEndDate: normalizeDate(t.planEndDate),
|
||||
children: (t.children || []).map((c: any) => ({
|
||||
...c,
|
||||
planStartDate: normalizeDate(c.planStartDate),
|
||||
planEndDate: normalizeDate(c.planEndDate),
|
||||
}))
|
||||
}))
|
||||
|
||||
const pickTaskFields = (t: any) => ({
|
||||
mainUserId: t.mainUserId ?? '',
|
||||
planEndDate: normalizeDate(t.planEndDate),
|
||||
planStartDate: normalizeDate(t.planStartDate),
|
||||
scales: t.scales ?? 0,
|
||||
taskCode: t.taskCode ?? '',
|
||||
taskGroupId: t.taskGroupId ?? '',
|
||||
taskName: t.taskName ?? '',
|
||||
})
|
||||
const flattenTasks = (tasks: any[]) => {
|
||||
const result: any[] = []
|
||||
;(tasks || []).forEach((t) => {
|
||||
result.push(pickTaskFields(t))
|
||||
;(t.children || []).forEach((c: any) => result.push(pickTaskFields(c)))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
...form,
|
||||
// 确保projectId字段正确
|
||||
auditorId: (form as any).auditorId || '',
|
||||
bonusProvision: (form as any).bonusProvision ?? 0,
|
||||
client: form.client || '',
|
||||
clientContact: form.clientContact || '',
|
||||
clientPhone: form.clientPhone || '',
|
||||
constructionTeamLeaderId: form.constructionTeamLeaderId || '',
|
||||
constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : (form.constructorIds || ''),
|
||||
coverUrl: (form as any).coverUrl || '',
|
||||
duration: (form as any).duration ?? 0,
|
||||
endDate: normalizeDate(form.endDate),
|
||||
equipmentAmortization: (form as any).equipmentAmortization ?? 0,
|
||||
farmAddress: form.farmAddress || '',
|
||||
farmName: form.farmName || '',
|
||||
inspectionContact: form.inspectionContact || '',
|
||||
inspectionPhone: form.inspectionPhone || '',
|
||||
inspectionUnit: form.inspectionUnit || '',
|
||||
laborCost: (form as any).laborCost ?? 0,
|
||||
othersCost: (form as any).othersCost ?? 0,
|
||||
projectBudget: (form as any).projectBudget ?? 0,
|
||||
projectId: isEdit.value && currentId.value ? currentId.value : form.projectId,
|
||||
// 处理日期格式 - 确保是字符串格式 YYYY-MM-DD
|
||||
startDate: form.startDate ? (typeof form.startDate === 'string' ? form.startDate : new Date(form.startDate).toISOString().split('T')[0]) : '',
|
||||
endDate: form.endDate ? (typeof form.endDate === 'string' ? form.endDate : new Date(form.endDate).toISOString().split('T')[0]) : '',
|
||||
// 处理施工人员ID - 如果是数组,转换为逗号分隔的字符串
|
||||
constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : form.constructorIds
|
||||
projectManagerId: form.projectManagerId || '',
|
||||
projectName: form.projectName,
|
||||
projectOrigin: (form as any).projectOrigin || '',
|
||||
qualityOfficerId: form.qualityOfficerId || '',
|
||||
scale: form.scale || '',
|
||||
startDate: normalizeDate(form.startDate),
|
||||
status: (form as any).status ?? 0,
|
||||
tasks: flattenTasks(form.tasks as any[]),
|
||||
transAccomMeals: (form as any).transAccomMeals ?? 0,
|
||||
turbineModel: form.turbineModel || '',
|
||||
}
|
||||
|
||||
console.log('提交数据:', submitData)
|
||||
|
@ -753,8 +989,8 @@ const viewDetail = (record: T.ProjectResp) => {
|
|||
router.push({
|
||||
name: 'ProjectDetail',
|
||||
params: {
|
||||
id: projectId.toString()
|
||||
}
|
||||
id: projectId.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -829,9 +1065,9 @@ const fetchUserList = async () => {
|
|||
// 调用用户列表接口
|
||||
const res = await http.get('/user/list')
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
userOptions.value = res.data.map(item => ({
|
||||
userOptions.value = res.data.map((item) => ({
|
||||
label: item.userName || item.username || item.name || item.nickName || item.account || '未命名用户',
|
||||
value: item.userId || item.id || ''
|
||||
value: item.userId || item.id || '',
|
||||
}))
|
||||
} else {
|
||||
userOptions.value = []
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="设备类型管理"
|
||||
width="600px"
|
||||
:confirm-loading="loading"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="类型名称" field="typeName" required>
|
||||
<a-input
|
||||
v-model="formData.typeName"
|
||||
placeholder="请输入设备类型名称"
|
||||
show-word-limit
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="类型编码" field="typeCode" required>
|
||||
<a-input
|
||||
v-model="formData.typeCode"
|
||||
placeholder="请输入设备类型编码"
|
||||
show-word-limit
|
||||
:max-length="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="类型描述" field="description">
|
||||
<a-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请输入设备类型描述"
|
||||
:rows="4"
|
||||
show-word-limit
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="排序" field="sort">
|
||||
<a-input-number
|
||||
v-model="formData.sort"
|
||||
placeholder="请输入排序值"
|
||||
:min="0"
|
||||
:max="999"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" field="status">
|
||||
<a-select
|
||||
v-model="formData.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="1">启用</a-option>
|
||||
<a-option value="0">禁用</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
typeData?: any
|
||||
mode: 'add' | 'edit'
|
||||
}
|
||||
|
||||
interface FormDataType {
|
||||
typeName: string
|
||||
typeCode: string
|
||||
description: string
|
||||
sort: number
|
||||
status: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
typeData: null,
|
||||
mode: 'add',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<FormDataType>({
|
||||
typeName: '',
|
||||
typeCode: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: '1'
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
typeName: [
|
||||
{ required: true, message: '请输入设备类型名称' },
|
||||
{ min: 2, max: 50, message: '类型名称长度应在2-50个字符之间' }
|
||||
],
|
||||
typeCode: [
|
||||
{ required: true, message: '请输入设备类型编码' },
|
||||
{ min: 2, max: 20, message: '类型编码长度应在2-20个字符之间' },
|
||||
{ pattern: /^[A-Z_]+$/, message: '类型编码只能包含大写字母和下划线' }
|
||||
],
|
||||
description: [
|
||||
{ max: 200, message: '类型描述不能超过200个字符' }
|
||||
],
|
||||
sort: [
|
||||
{ type: 'number', min: 0, max: 999, message: '排序值应在0-999之间' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => props.typeData, (newData) => {
|
||||
if (newData) {
|
||||
Object.assign(formData, {
|
||||
typeName: newData.typeName || '',
|
||||
typeCode: newData.typeCode || '',
|
||||
description: newData.description || '',
|
||||
sort: newData.sort || 0,
|
||||
status: newData.status || '1'
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 监听弹窗显示状态
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible && props.mode === 'add') {
|
||||
// 新增模式时重置表单
|
||||
Object.assign(formData, {
|
||||
typeName: '',
|
||||
typeCode: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: '1'
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
// 这里应该调用API保存数据
|
||||
// 暂时模拟保存成功
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
Message.success(props.mode === 'add' ? '设备类型添加成功' : '设备类型更新成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
Message.error('保存失败,请检查表单信息')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.arco-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.arco-input,
|
||||
.arco-textarea,
|
||||
.arco-select,
|
||||
.arco-input-number {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.arco-textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
</style>
|
|
@ -19,6 +19,12 @@
|
|||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
<a-button type="outline" @click="handleAddEquipmentType" size="large">
|
||||
<template #icon>
|
||||
<IconTag />
|
||||
</template>
|
||||
添加设备类型
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleAdd" size="large">
|
||||
<template #icon>
|
||||
<IconPlus />
|
||||
|
@ -235,6 +241,14 @@
|
|||
:mode="modalMode"
|
||||
@success="handleModalSuccess"
|
||||
/>
|
||||
|
||||
<!-- 设备类型管理弹窗 -->
|
||||
<EquipmentTypeModal
|
||||
v-model:visible="equipmentTypeModalVisible"
|
||||
:type-data="currentEquipmentType"
|
||||
:mode="equipmentTypeModalMode"
|
||||
@success="handleEquipmentTypeModalSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -250,9 +264,11 @@ import {
|
|||
IconPlus,
|
||||
IconRefresh,
|
||||
IconSearch,
|
||||
IconTag,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import message from '@arco-design/web-vue/es/message'
|
||||
import DeviceModal from './components/DeviceModal.vue'
|
||||
import EquipmentTypeModal from './components/EquipmentTypeModal.vue'
|
||||
import EquipmentSearch from './components/EquipmentSearch.vue'
|
||||
import router from '@/router'
|
||||
import { EquipmentAPI } from '@/apis'
|
||||
|
@ -287,6 +303,11 @@ const modalVisible = ref(false)
|
|||
const currentEquipment = ref<EquipmentResp | null>(null)
|
||||
const modalMode = ref<'add' | 'edit' | 'view'>('add')
|
||||
|
||||
// 设备类型弹窗控制
|
||||
const equipmentTypeModalVisible = ref(false)
|
||||
const currentEquipmentType = ref<any>(null)
|
||||
const equipmentTypeModalMode = ref<'add' | 'edit'>('add')
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
|
@ -913,6 +934,19 @@ const handleModalSuccess = () => {
|
|||
loadData()
|
||||
}
|
||||
|
||||
// 新增设备类型
|
||||
const handleAddEquipmentType = () => {
|
||||
equipmentTypeModalMode.value = 'add'
|
||||
currentEquipmentType.value = null
|
||||
equipmentTypeModalVisible.value = true
|
||||
}
|
||||
|
||||
// 设备类型弹窗成功回调
|
||||
const handleEquipmentTypeModalSuccess = () => {
|
||||
equipmentTypeModalVisible.value = false
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 监听表格数据变化
|
||||
watch(tableData, (_newData) => {
|
||||
// 数据变化监听
|
||||
|
|
|
@ -0,0 +1,330 @@
|
|||
<template>
|
||||
<div class="equipment-inventory">
|
||||
<a-card title="设备盘库管理" :bordered="false">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleBatchInventory">
|
||||
<template #icon>
|
||||
<IconPlus />
|
||||
</template>
|
||||
批量盘库
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<a-form layout="inline" :model="searchForm" class="search-form">
|
||||
<a-form-item label="设备名称">
|
||||
<a-input
|
||||
v-model="searchForm.equipmentName"
|
||||
placeholder="请输入设备名称"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="资产编号">
|
||||
<a-input
|
||||
v-model="searchForm.assetCode"
|
||||
placeholder="请输入资产编号"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="设备类型">
|
||||
<a-select
|
||||
v-model="searchForm.equipmentType"
|
||||
placeholder="请选择类型"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
@change="debouncedSearch"
|
||||
>
|
||||
<a-option value="">全部</a-option>
|
||||
<a-option value="COMPUTER">计算机设备</a-option>
|
||||
<a-option value="NETWORK">网络设备</a-option>
|
||||
<a-option value="STORAGE">存储设备</a-option>
|
||||
<a-option value="SECURITY">安防设备</a-option>
|
||||
<a-option value="OTHER">其他设备</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="search">
|
||||
<template #icon><IconSearch /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="reset">
|
||||
<template #icon><IconRefresh /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-selection="rowSelection"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<span class="search-result-info">
|
||||
共找到 <strong>{{ pagination.total }}</strong> 条记录
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #equipmentStatus="{ record }">
|
||||
<a-tag :color="getStatusColor(record.equipmentStatus)">
|
||||
{{ getStatusText(record.equipmentStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template #operations="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="handleView(record)">
|
||||
查看详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleInventory(record)">
|
||||
执行盘库
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconSearch, IconRefresh, IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import { pageEquipmentInventory } from '@/apis/equipment'
|
||||
import type { EquipmentResp } from '@/types/equipment.d'
|
||||
|
||||
// 防抖函数
|
||||
const debounce = (func: Function, delay: number) => {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => func.apply(null, args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
defineOptions({ name: 'EquipmentInventory' })
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: '设备名称', dataIndex: 'equipmentName', key: 'equipmentName', width: 180 },
|
||||
{ title: '资产编号', dataIndex: 'assetCode', key: 'assetCode', width: 150 },
|
||||
{ title: '设备类型', dataIndex: 'equipmentType', key: 'equipmentType', width: 120 },
|
||||
{ title: '品牌', dataIndex: 'brand', key: 'brand', width: 100 },
|
||||
{ title: '型号', dataIndex: 'equipmentModel', key: 'equipmentModel', width: 120 },
|
||||
{ title: '设备状态', dataIndex: 'equipmentStatus', key: 'equipmentStatus', slotName: 'equipmentStatus', width: 100 },
|
||||
{ title: '负责人', dataIndex: 'responsiblePerson', key: 'responsiblePerson', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
|
||||
{ title: '操作', key: 'operations', slotName: 'operations', width: 200 }
|
||||
]
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
equipmentName: '',
|
||||
assetCode: '',
|
||||
equipmentType: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<EquipmentResp[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showTotal: true,
|
||||
showJumper: true,
|
||||
showPageSize: true
|
||||
})
|
||||
|
||||
// 表格选择
|
||||
const selectedRowKeys = ref<string[]>([])
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys: string[]) => {
|
||||
selectedRowKeys.value = keys
|
||||
}
|
||||
}))
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
'IDLE': 'blue',
|
||||
'IN_USE': 'green',
|
||||
'MAINTENANCE': 'orange',
|
||||
'REPAIR': 'red',
|
||||
'SCRAPPED': 'gray'
|
||||
}
|
||||
return colors[status] || 'blue'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const texts: Record<string, string> = {
|
||||
'IDLE': '空闲中',
|
||||
'IN_USE': '使用中',
|
||||
'MAINTENANCE': '保养中',
|
||||
'REPAIR': '维修中',
|
||||
'SCRAPPED': '已报废'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取表格数据
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await pageEquipmentInventory({
|
||||
pageNum: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
equipmentName: searchForm.equipmentName || undefined,
|
||||
assetCode: searchForm.assetCode || undefined,
|
||||
equipmentType: searchForm.equipmentType || undefined
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
// 检查响应数据结构
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
// 如果直接返回数组,说明没有分页信息
|
||||
tableData.value = response.data
|
||||
pagination.total = response.data.length
|
||||
} else if (response.data && typeof response.data === 'object' && 'records' in response.data) {
|
||||
// 如果有分页信息
|
||||
const pageData = response.data as any
|
||||
tableData.value = pageData.records || []
|
||||
pagination.total = pageData.total || pageData.records?.length || 0
|
||||
pagination.current = pageData.current || 1
|
||||
} else {
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} else {
|
||||
Message.error('获取数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备盘库列表失败:', error)
|
||||
Message.error('获取数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖搜索函数
|
||||
const debouncedSearch = debounce(() => {
|
||||
pagination.current = 1
|
||||
getTableData()
|
||||
}, 300)
|
||||
|
||||
// 搜索
|
||||
const search = () => {
|
||||
pagination.current = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
Object.assign(searchForm, {
|
||||
equipmentName: '',
|
||||
assetCode: '',
|
||||
equipmentType: ''
|
||||
})
|
||||
pagination.current = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleView = (record: EquipmentResp) => {
|
||||
console.log('查看设备详情:', record)
|
||||
}
|
||||
|
||||
// 执行盘库
|
||||
const handleInventory = (record: EquipmentResp) => {
|
||||
console.log('执行设备盘库:', record)
|
||||
}
|
||||
|
||||
// 批量盘库
|
||||
const handleBatchInventory = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
Message.warning('请先选择要盘库的设备')
|
||||
return
|
||||
}
|
||||
console.log('批量盘库设备:', selectedRowKeys.value)
|
||||
}
|
||||
|
||||
// 分页事件
|
||||
const handlePageChange = (page: number) => {
|
||||
pagination.current = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.current = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.equipment-inventory {
|
||||
.arco-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 16px;
|
||||
background: var(--color-bg-2);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.search-form {
|
||||
.arco-form-item {
|
||||
margin-bottom: 0;
|
||||
margin-right: 16px;
|
||||
|
||||
.arco-form-item-label {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-input,
|
||||
.arco-select {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.arco-btn {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-info {
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
|
||||
strong {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,249 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="支付详情"
|
||||
width="800px"
|
||||
@cancel="handleCancel"
|
||||
:footer="false"
|
||||
>
|
||||
<div class="payment-detail">
|
||||
<!-- 基本信息 -->
|
||||
<a-card title="基本信息" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="设备名称">
|
||||
{{ paymentData?.equipmentName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备类型">
|
||||
{{ paymentData?.equipmentType || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备型号">
|
||||
{{ paymentData?.equipmentModel || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="品牌">
|
||||
{{ paymentData?.brand || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商">
|
||||
{{ paymentData?.supplierName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购订单">
|
||||
{{ paymentData?.purchaseOrder || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 采购信息 -->
|
||||
<a-card title="采购信息" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="采购价格">
|
||||
¥{{ paymentData?.purchasePrice || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购数量">
|
||||
{{ paymentData?.quantity || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="总金额">
|
||||
¥{{ paymentData?.totalPrice || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购日期">
|
||||
{{ paymentData?.purchaseTime || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 支付信息 -->
|
||||
<a-card title="支付信息" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="支付状态">
|
||||
<a-tag :color="getPaymentStatusColor(paymentData?.paymentStatus)">
|
||||
{{ getPaymentStatusText(paymentData?.paymentStatus) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="支付方式">
|
||||
{{ paymentData?.paymentMethod || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="支付金额">
|
||||
¥{{ paymentData?.paymentAmount || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="支付时间">
|
||||
{{ paymentData?.paymentTime || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="支付人">
|
||||
{{ paymentData?.paymentPerson || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="支付备注">
|
||||
{{ paymentData?.paymentRemark || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 发票信息 -->
|
||||
<a-card title="发票信息" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="发票状态">
|
||||
<a-tag :color="getInvoiceStatusColor(paymentData?.invoiceStatus)">
|
||||
{{ getInvoiceStatusText(paymentData?.invoiceStatus) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发票类型">
|
||||
{{ paymentData?.invoiceType || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发票号码">
|
||||
{{ paymentData?.invoiceNumber || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="开票日期">
|
||||
{{ paymentData?.invoiceDate || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发票金额">
|
||||
¥{{ paymentData?.invoiceAmount || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="税率">
|
||||
{{ paymentData?.taxRate || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="税额">
|
||||
¥{{ paymentData?.taxAmount || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="不含税金额">
|
||||
¥{{ paymentData?.amountWithoutTax || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 合同信息 -->
|
||||
<a-card title="合同信息" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="合同编号">
|
||||
{{ paymentData?.contractNumber || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="合同状态">
|
||||
<a-tag :color="getContractStatusColor(paymentData?.contractStatus)">
|
||||
{{ getContractStatusText(paymentData?.contractStatus) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="合同金额">
|
||||
¥{{ paymentData?.contractAmount || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="签订日期">
|
||||
{{ paymentData?.contractDate || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="付款条件">
|
||||
{{ paymentData?.paymentTerms || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="付款期限">
|
||||
{{ paymentData?.paymentDeadline || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { EquipmentResp } from '@/apis/equipment/type'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
paymentData?: EquipmentResp | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
paymentData: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
}>()
|
||||
|
||||
// 获取支付状态颜色
|
||||
const getPaymentStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
NOT_PAID: 'gray',
|
||||
PAID: 'green',
|
||||
PARTIALLY_PAID: 'orange',
|
||||
REJECTED: 'red',
|
||||
}
|
||||
return colorMap[status || ''] || 'blue'
|
||||
}
|
||||
|
||||
// 获取支付状态文本
|
||||
const getPaymentStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
NOT_PAID: '未支付',
|
||||
PAID: '已支付',
|
||||
PARTIALLY_PAID: '部分支付',
|
||||
REJECTED: '已拒付',
|
||||
}
|
||||
return textMap[status || ''] || '未知'
|
||||
}
|
||||
|
||||
// 获取发票状态颜色
|
||||
const getInvoiceStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
NOT_ISSUED: 'gray',
|
||||
ISSUED: 'green',
|
||||
PENDING: 'orange',
|
||||
REJECTED: 'red',
|
||||
}
|
||||
return colorMap[status || ''] || 'blue'
|
||||
}
|
||||
|
||||
// 获取发票状态文本
|
||||
const getInvoiceStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
NOT_ISSUED: '未开票',
|
||||
ISSUED: '已开票',
|
||||
PENDING: '待开票',
|
||||
REJECTED: '已拒开',
|
||||
}
|
||||
return textMap[status || ''] || '未知'
|
||||
}
|
||||
|
||||
// 获取合同状态颜色
|
||||
const getContractStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
DRAFT: 'gray',
|
||||
SIGNED: 'green',
|
||||
EXECUTING: 'blue',
|
||||
COMPLETED: 'purple',
|
||||
TERMINATED: 'red',
|
||||
}
|
||||
return colorMap[status || ''] || 'blue'
|
||||
}
|
||||
|
||||
// 获取合同状态文本
|
||||
const getContractStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
DRAFT: '草稿',
|
||||
SIGNED: '已签订',
|
||||
EXECUTING: '执行中',
|
||||
COMPLETED: '已完成',
|
||||
TERMINATED: '已终止',
|
||||
}
|
||||
return textMap[status || ''] || '未知'
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-detail {
|
||||
.detail-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-descriptions-item-label {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.arco-descriptions-item-value {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,505 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="设备付款"
|
||||
width="900px"
|
||||
:confirm-loading="loading"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
: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-item label="设备名称">
|
||||
{{ equipmentData?.equipmentName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备类型">
|
||||
{{ equipmentData?.equipmentType || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备型号">
|
||||
{{ equipmentData?.equipmentModel || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="品牌">
|
||||
{{ equipmentData?.brand || '-' }}
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-descriptions-item label="供应商">
|
||||
{{ equipmentData?.supplierName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购订单">
|
||||
{{ equipmentData?.purchaseOrder || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购价格">
|
||||
¥{{ equipmentData?.purchasePrice || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购数量">
|
||||
{{ equipmentData?.quantity || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 支付信息 -->
|
||||
<a-card title="支付信息" class="detail-card" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支付方式" field="paymentMethod" required>
|
||||
<a-select
|
||||
v-model="formData.paymentMethod"
|
||||
placeholder="请选择支付方式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="银行转账">银行转账</a-option>
|
||||
<a-option value="现金">现金</a-option>
|
||||
<a-option value="支票">支票</a-option>
|
||||
<a-option value="信用卡">信用卡</a-option>
|
||||
<a-option value="其他">其他</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</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 :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="支付时间" field="paymentTime" required>
|
||||
<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
|
||||
v-model="formData.paymentRemark"
|
||||
placeholder="请输入支付备注"
|
||||
:rows="3"
|
||||
show-word-limit
|
||||
:max-length="200"
|
||||
/>
|
||||
</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="invoiceType" required>
|
||||
<a-select
|
||||
v-model="formData.invoiceType"
|
||||
placeholder="请选择发票类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="增值税专用发票">增值税专用发票</a-option>
|
||||
<a-option value="增值税普通发票">增值税普通发票</a-option>
|
||||
<a-option value="电子发票">电子发票</a-option>
|
||||
<a-option value="其他">其他</a-option>
|
||||
</a-select>
|
||||
</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="开票日期" field="invoiceDate" required>
|
||||
<a-date-picker
|
||||
v-model="formData.invoiceDate"
|
||||
format="YYYY-MM-DD"
|
||||
placeholder="请选择开票日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</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-col :span="12">
|
||||
<a-form-item label="税率(%)" field="taxRate" required>
|
||||
<a-input-number
|
||||
v-model="formData.taxRate"
|
||||
placeholder="请输入税率"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</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-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import type { EquipmentResp, PaymentRequest } from '@/apis/equipment/type'
|
||||
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
equipmentData?: EquipmentResp | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
equipmentData: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<PaymentRequest>({
|
||||
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(() => {
|
||||
if (formData.invoiceAmount && formData.taxRate) {
|
||||
return (formData.invoiceAmount * formData.taxRate) / 100
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const calculatedAmountWithoutTax = computed(() => {
|
||||
if (formData.invoiceAmount && formData.taxAmount) {
|
||||
return formData.invoiceAmount - formData.taxAmount
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// 监听发票金额和税率变化,自动计算税额和不含税金额
|
||||
watch([() => formData.invoiceAmount, () => formData.taxRate], () => {
|
||||
formData.taxAmount = calculatedTaxAmount.value
|
||||
formData.amountWithoutTax = calculatedAmountWithoutTax.value
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
paymentMethod: [
|
||||
{ required: true, message: '请选择支付方式' }
|
||||
],
|
||||
paymentAmount: [
|
||||
{ required: true, message: '请输入支付金额' },
|
||||
{ type: 'number', min: 0.01, message: '支付金额必须大于0' }
|
||||
],
|
||||
paymentTime: [
|
||||
{ required: true, message: '请选择支付时间' }
|
||||
],
|
||||
paymentPerson: [
|
||||
{ required: true, message: '请输入支付人姓名' },
|
||||
{ min: 2, max: 50, message: '支付人姓名长度应在2-50个字符之间' }
|
||||
],
|
||||
invoiceType: [
|
||||
{ required: true, message: '请选择发票类型' }
|
||||
],
|
||||
invoiceNumber: [
|
||||
{ required: true, message: '请输入发票号码' },
|
||||
{ min: 2, max: 50, message: '发票号码长度应在2-50个字符之间' }
|
||||
],
|
||||
invoiceDate: [
|
||||
{ required: true, message: '请选择开票日期' }
|
||||
],
|
||||
invoiceAmount: [
|
||||
{ required: true, message: '请输入发票金额' },
|
||||
{ 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: '请选择付款期限' }
|
||||
],
|
||||
}
|
||||
|
||||
// 监听弹窗显示状态
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible && props.equipmentData) {
|
||||
// 重置表单并设置默认值
|
||||
Object.assign(formData, {
|
||||
paymentMethod: '',
|
||||
paymentAmount: props.equipmentData.purchasePrice || 0,
|
||||
paymentTime: '',
|
||||
paymentPerson: '',
|
||||
paymentRemark: '',
|
||||
invoiceType: '增值税专用发票',
|
||||
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 invoiceDate = formData.invoiceDate ? new Date(formData.invoiceDate).toISOString() : new Date().toISOString()
|
||||
const contractDate = formData.contractDate ? new Date(formData.contractDate).toISOString() : new Date().toISOString()
|
||||
const paymentDeadline = formData.paymentDeadline ? new Date(formData.paymentDeadline).toISOString() : new Date().toISOString()
|
||||
|
||||
const requestData: PaymentRequest = {
|
||||
...formData,
|
||||
paymentTime,
|
||||
invoiceDate,
|
||||
contractDate,
|
||||
paymentDeadline,
|
||||
}
|
||||
|
||||
await equipmentProcurementApi.makePayment(props.equipmentData.equipmentId, requestData)
|
||||
|
||||
Message.success('付款成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error: any) {
|
||||
console.error('付款失败:', error)
|
||||
Message.error(error?.message || '付款失败,请检查表单信息')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.arco-input,
|
||||
.arco-select,
|
||||
.arco-input-number,
|
||||
.arco-date-picker,
|
||||
.arco-textarea {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.arco-textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,211 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="收货详情"
|
||||
width="800px"
|
||||
@cancel="handleCancel"
|
||||
:footer="false"
|
||||
>
|
||||
<div class="receipt-detail">
|
||||
<!-- 基本信息 -->
|
||||
<a-card title="基本信息" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="设备名称">
|
||||
{{ receiptData?.equipmentName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备类型">
|
||||
{{ receiptData?.equipmentType || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备型号">
|
||||
{{ receiptData?.equipmentModel || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="品牌">
|
||||
{{ receiptData?.brand || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商">
|
||||
{{ receiptData?.supplierName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购订单">
|
||||
{{ receiptData?.purchaseOrder || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 收货信息 -->
|
||||
<a-card title="收货信息" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="收货状态">
|
||||
<a-tag :color="getReceiptStatusColor(receiptData?.receiptStatus)">
|
||||
{{ getReceiptStatusText(receiptData?.receiptStatus) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="收货时间">
|
||||
{{ receiptData?.receiptTime || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="收货人">
|
||||
{{ receiptData?.receiptPerson || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="收货数量">
|
||||
{{ receiptData?.receiptQuantity || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="收货备注" :span="2">
|
||||
{{ receiptData?.receiptRemark || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 质量检查 -->
|
||||
<a-card title="质量检查" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="外观检查">
|
||||
{{ receiptData?.appearanceCheck || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="功能测试">
|
||||
{{ receiptData?.functionTest || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="包装完整性">
|
||||
{{ receiptData?.packageIntegrity || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="配件完整性">
|
||||
{{ receiptData?.accessoryIntegrity || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="检查结果" :span="2">
|
||||
<a-tag :color="getCheckResultColor(receiptData?.checkResult)">
|
||||
{{ getCheckResultText(receiptData?.checkResult) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="检查备注" :span="2">
|
||||
{{ receiptData?.checkRemark || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 入库信息 -->
|
||||
<a-card title="入库信息" class="detail-card" :bordered="false">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="入库状态">
|
||||
<a-tag :color="getStorageStatusColor(receiptData?.storageStatus)">
|
||||
{{ getStorageStatusText(receiptData?.storageStatus) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="入库时间">
|
||||
{{ receiptData?.storageTime || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="入库位置">
|
||||
{{ receiptData?.storageLocation || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="库管员">
|
||||
{{ receiptData?.storageManager || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { EquipmentResp } from '@/apis/equipment/type'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
receiptData?: EquipmentResp | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
receiptData: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
}>()
|
||||
|
||||
// 获取收货状态颜色
|
||||
const getReceiptStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
NOT_RECEIVED: 'gray',
|
||||
RECEIVED: 'green',
|
||||
PARTIALLY_RECEIVED: 'orange',
|
||||
REJECTED: 'red',
|
||||
}
|
||||
return colorMap[status || ''] || 'blue'
|
||||
}
|
||||
|
||||
// 获取收货状态文本
|
||||
const getReceiptStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
NOT_RECEIVED: '未收货',
|
||||
RECEIVED: '已收货',
|
||||
PARTIALLY_RECEIVED: '部分收货',
|
||||
REJECTED: '已拒收',
|
||||
}
|
||||
return textMap[status || ''] || '未知'
|
||||
}
|
||||
|
||||
// 获取检查结果颜色
|
||||
const getCheckResultColor = (result?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
PASS: 'green',
|
||||
FAIL: 'red',
|
||||
CONDITIONAL: 'orange',
|
||||
}
|
||||
return colorMap[result || ''] || 'blue'
|
||||
}
|
||||
|
||||
// 获取检查结果文本
|
||||
const getCheckResultText = (result?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
PASS: '通过',
|
||||
FAIL: '不通过',
|
||||
CONDITIONAL: '有条件通过',
|
||||
}
|
||||
return textMap[result || ''] || '未知'
|
||||
}
|
||||
|
||||
// 获取入库状态颜色
|
||||
const getStorageStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
NOT_STORED: 'gray',
|
||||
STORED: 'green',
|
||||
PARTIALLY_STORED: 'orange',
|
||||
}
|
||||
return colorMap[status || ''] || 'blue'
|
||||
}
|
||||
|
||||
// 获取入库状态文本
|
||||
const getStorageStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
NOT_STORED: '未入库',
|
||||
STORED: '已入库',
|
||||
PARTIALLY_STORED: '部分入库',
|
||||
}
|
||||
return textMap[status || ''] || '未知'
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.receipt-detail {
|
||||
.detail-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-descriptions-item-label {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.arco-descriptions-item-value {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,509 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="确认收货"
|
||||
width="800px"
|
||||
:confirm-loading="loading"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
: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-item label="设备名称">
|
||||
{{ equipmentData?.equipmentName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备类型">
|
||||
{{ equipmentData?.equipmentType || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备型号">
|
||||
{{ equipmentData?.equipmentModel || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="品牌">
|
||||
{{ equipmentData?.brand || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商">
|
||||
{{ equipmentData?.supplierName || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采购订单">
|
||||
{{ equipmentData?.purchaseOrder || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<!-- 收货信息 -->
|
||||
<a-card title="收货信息" class="detail-card" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="收货时间" field="receiptTime" required>
|
||||
<a-date-picker
|
||||
v-model="formData.receiptTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-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="receiptPerson" required>
|
||||
<a-input
|
||||
v-model="formData.receiptPerson"
|
||||
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="收货数量" field="receiptQuantity" required>
|
||||
<a-input-number
|
||||
v-model="formData.receiptQuantity"
|
||||
placeholder="请输入收货数量"
|
||||
:min="1"
|
||||
:max="9999"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="入库位置" field="storageLocation" required>
|
||||
<a-input
|
||||
v-model="formData.storageLocation"
|
||||
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="storageManager" required>
|
||||
<a-input
|
||||
v-model="formData.storageManager"
|
||||
placeholder="请输入库管员姓名"
|
||||
show-word-limit
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="收货备注" field="receiptRemark">
|
||||
<a-input
|
||||
v-model="formData.receiptRemark"
|
||||
placeholder="请输入收货备注"
|
||||
show-word-limit
|
||||
:max-length="200"
|
||||
/>
|
||||
</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="appearanceCheck" required>
|
||||
<a-select
|
||||
v-model="formData.appearanceCheck"
|
||||
placeholder="请选择外观检查结果"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="正常">正常</a-option>
|
||||
<a-option value="轻微划痕">轻微划痕</a-option>
|
||||
<a-option value="明显划痕">明显划痕</a-option>
|
||||
<a-option value="变形">变形</a-option>
|
||||
<a-option value="其他">其他</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="功能测试" field="functionTest" required>
|
||||
<a-select
|
||||
v-model="formData.functionTest"
|
||||
placeholder="请选择功能测试结果"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="正常">正常</a-option>
|
||||
<a-option value="部分正常">部分正常</a-option>
|
||||
<a-option value="异常">异常</a-option>
|
||||
<a-option value="未测试">未测试</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="包装完整性" field="packageIntegrity" required>
|
||||
<a-select
|
||||
v-model="formData.packageIntegrity"
|
||||
placeholder="请选择包装完整性"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="完整">完整</a-option>
|
||||
<a-option value="轻微破损">轻微破损</a-option>
|
||||
<a-option value="明显破损">明显破损</a-option>
|
||||
<a-option value="严重破损">严重破损</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="配件完整性" field="accessoryIntegrity" required>
|
||||
<a-select
|
||||
v-model="formData.accessoryIntegrity"
|
||||
placeholder="请选择配件完整性"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="完整">完整</a-option>
|
||||
<a-option value="部分缺失">部分缺失</a-option>
|
||||
<a-option value="严重缺失">严重缺失</a-option>
|
||||
<a-option value="无配件">无配件</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="检查结果" field="checkResult" required>
|
||||
<a-select
|
||||
v-model="formData.checkResult"
|
||||
placeholder="请选择检查结果"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-option value="PASS">通过</a-option>
|
||||
<a-option value="FAIL">不通过</a-option>
|
||||
<a-option value="CONDITIONAL">有条件通过</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="检查备注" field="checkRemark">
|
||||
<a-input
|
||||
v-model="formData.checkRemark"
|
||||
placeholder="请输入检查备注"
|
||||
show-word-limit
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import type { EquipmentResp, ReceiptRequest } from '@/apis/equipment/type'
|
||||
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
equipmentData?: EquipmentResp | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
equipmentData: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单数据 - 使用正确的字段映射
|
||||
const formData = reactive<ReceiptRequest>({
|
||||
// 收货特有信息
|
||||
receiptTime: '',
|
||||
receiptPerson: '',
|
||||
receiptQuantity: 1,
|
||||
receiptRemark: '',
|
||||
appearanceCheck: '',
|
||||
functionTest: '',
|
||||
packageIntegrity: '',
|
||||
accessoryIntegrity: '',
|
||||
checkResult: 'PASS',
|
||||
checkRemark: '',
|
||||
storageLocation: '',
|
||||
storageManager: '',
|
||||
|
||||
// 设备基本信息(从采购数据继承)
|
||||
equipmentName: '',
|
||||
equipmentModel: '',
|
||||
equipmentType: '',
|
||||
equipmentSn: '',
|
||||
brand: '',
|
||||
specification: '',
|
||||
assetCode: '',
|
||||
|
||||
// 采购信息(从采购数据继承)
|
||||
purchaseOrder: '',
|
||||
supplierName: '',
|
||||
purchasePrice: 0,
|
||||
purchaseTime: '',
|
||||
quantity: 1,
|
||||
unitPrice: 0,
|
||||
totalPrice: 0,
|
||||
|
||||
// 入库信息
|
||||
inStockTime: '',
|
||||
physicalLocation: '',
|
||||
locationStatus: '',
|
||||
responsiblePerson: '',
|
||||
inventoryBarcode: '',
|
||||
|
||||
// 状态信息
|
||||
equipmentStatus: '',
|
||||
useStatus: '',
|
||||
healthStatus: '',
|
||||
receiptStatus: '',
|
||||
|
||||
// 其他管理信息
|
||||
depreciationMethod: '',
|
||||
depreciationYears: 5,
|
||||
salvageValue: 0,
|
||||
currentNetValue: 0,
|
||||
|
||||
// 系统字段
|
||||
createTime: '',
|
||||
updateTime: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
receiptTime: [
|
||||
{ required: true, message: '请选择收货时间' }
|
||||
],
|
||||
receiptPerson: [
|
||||
{ required: true, message: '请输入收货人姓名' },
|
||||
{ min: 2, max: 50, message: '收货人姓名长度应在2-50个字符之间' }
|
||||
],
|
||||
receiptQuantity: [
|
||||
{ required: true, message: '请输入收货数量' },
|
||||
{ type: 'number', min: 1, max: 9999, message: '收货数量应在1-9999之间' }
|
||||
],
|
||||
storageLocation: [
|
||||
{ required: true, message: '请输入入库位置' },
|
||||
{ min: 2, max: 100, message: '入库位置长度应在2-100个字符之间' }
|
||||
],
|
||||
storageManager: [
|
||||
{ required: true, message: '请输入库管员姓名' },
|
||||
{ min: 2, max: 50, message: '库管员姓名长度应在2-50个字符之间' }
|
||||
],
|
||||
appearanceCheck: [
|
||||
{ required: true, message: '请选择外观检查结果' }
|
||||
],
|
||||
functionTest: [
|
||||
{ required: true, message: '请选择功能测试结果' }
|
||||
],
|
||||
packageIntegrity: [
|
||||
{ required: true, message: '请选择包装完整性' }
|
||||
],
|
||||
accessoryIntegrity: [
|
||||
{ required: true, message: '请选择配件完整性' }
|
||||
],
|
||||
checkResult: [
|
||||
{ required: true, message: '请选择检查结果' }
|
||||
],
|
||||
}
|
||||
|
||||
// 初始化表单数据
|
||||
const initFormData = () => {
|
||||
if (props.equipmentData) {
|
||||
// 从设备数据中复制相关字段
|
||||
Object.keys(formData).forEach((key) => {
|
||||
const formKey = key as keyof ReceiptRequest
|
||||
const equipmentKey = key as keyof EquipmentResp
|
||||
if (formKey in formData && equipmentKey in props.equipmentData!) {
|
||||
const value = props.equipmentData![equipmentKey]
|
||||
if (value !== undefined) {
|
||||
(formData[formKey] as any) = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 设置默认值
|
||||
formData.receiptQuantity = props.equipmentData.quantity || 1
|
||||
formData.storageLocation = props.equipmentData.physicalLocation || ''
|
||||
formData.storageManager = props.equipmentData.responsiblePerson || ''
|
||||
|
||||
// 设置收货时间默认为当前时间
|
||||
formData.receiptTime = formatDateTime(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
// 监听弹窗显示状态
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
initFormData()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听设备数据变化
|
||||
watch(() => props.equipmentData, () => {
|
||||
if (props.visible && props.equipmentData) {
|
||||
initFormData()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
loading.value = true
|
||||
|
||||
if (!props.equipmentData?.equipmentId) {
|
||||
throw new Error('设备ID不能为空')
|
||||
}
|
||||
|
||||
console.log('📦 开始提交收货数据...')
|
||||
console.log('📦 设备数据:', props.equipmentData)
|
||||
console.log('📦 表单数据:', formData)
|
||||
|
||||
// 构建收货请求数据
|
||||
const receiptData: ReceiptRequest = {
|
||||
// 收货特有信息
|
||||
receiptTime: formData.receiptTime ? formatDateTime(formData.receiptTime) : formatDateTime(new Date()),
|
||||
receiptPerson: formData.receiptPerson,
|
||||
receiptQuantity: formData.receiptQuantity,
|
||||
receiptRemark: formData.receiptRemark,
|
||||
appearanceCheck: formData.appearanceCheck,
|
||||
functionTest: formData.functionTest,
|
||||
packageIntegrity: formData.packageIntegrity,
|
||||
accessoryIntegrity: formData.accessoryIntegrity,
|
||||
checkResult: formData.checkResult,
|
||||
checkRemark: formData.checkRemark,
|
||||
storageLocation: formData.storageLocation,
|
||||
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',
|
||||
useStatus: '0',
|
||||
healthStatus: 'good',
|
||||
receiptStatus: 'RECEIVED',
|
||||
|
||||
// 其他管理信息
|
||||
depreciationMethod: props.equipmentData.depreciationMethod || 'straight_line',
|
||||
depreciationYears: props.equipmentData.depreciationYears || 5,
|
||||
salvageValue: props.equipmentData.salvageValue || 0,
|
||||
currentNetValue: props.equipmentData.purchasePrice || 0,
|
||||
|
||||
// 系统字段
|
||||
createTime: formatDateTime(new Date()),
|
||||
updateTime: formatDateTime(new Date())
|
||||
}
|
||||
|
||||
console.log('📦 构建的收货数据:', receiptData)
|
||||
|
||||
// 调用收货API
|
||||
await equipmentProcurementApi.receiveGoods(
|
||||
props.equipmentData.equipmentId,
|
||||
receiptData
|
||||
)
|
||||
|
||||
Message.success('收货成功,设备已自动入库')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} catch (error: any) {
|
||||
console.error('收货失败:', error)
|
||||
Message.error(error?.message || '收货失败,请检查表单信息')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成库存条码的辅助函数
|
||||
const generateInventoryBarcode = () => {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const random = Math.random().toString(36).substr(2, 5)
|
||||
return `INV-${timestamp}-${random}`.toUpperCase()
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (date: string | Date) => {
|
||||
const d = new Date(date);
|
||||
let month = '' + (d.getMonth() + 1);
|
||||
let day = '' + d.getDate();
|
||||
const year = d.getFullYear();
|
||||
if (month.length < 2)
|
||||
month = '0' + month;
|
||||
if (day.length < 2)
|
||||
day = '0' + day;
|
||||
return [year, month, day].join('-') + ' ' + [d.getHours(), d.getMinutes(), d.getSeconds()].join(':');
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.arco-input,
|
||||
.arco-select,
|
||||
.arco-input-number,
|
||||
.arco-date-picker {
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
|
@ -178,6 +178,20 @@
|
|||
<span v-else class="no-data">-</span>
|
||||
</template>
|
||||
|
||||
<!-- 收货状态 -->
|
||||
<template #receiptStatus="{ record }">
|
||||
<a-tag :color="getReceiptStatusColor(record.receiptStatus)">
|
||||
{{ getReceiptStatusText(record.receiptStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 支付状态 -->
|
||||
<template #paymentStatus="{ record }">
|
||||
<a-tag :color="getPaymentStatusColor(record.paymentStatus)">
|
||||
{{ getPaymentStatusText(record.paymentStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
|
@ -196,6 +210,40 @@
|
|||
>
|
||||
申请采购
|
||||
</a-button>
|
||||
<!-- 收货操作按钮 -->
|
||||
<a-button
|
||||
v-if="canReceiveGoods(record)"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleReceiveGoods(record)"
|
||||
>
|
||||
确认收货
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.receiptStatus === 'RECEIVED'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="handleViewReceipt(record)"
|
||||
>
|
||||
查看收货
|
||||
</a-button>
|
||||
<!-- 支付操作按钮 -->
|
||||
<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>
|
||||
<!-- 显示采购状态 - 优先显示采购状态 -->
|
||||
<a-tag
|
||||
v-if="record.procurementStatus && record.procurementStatus !== 'NOT_STARTED'"
|
||||
|
@ -252,6 +300,32 @@
|
|||
:equipment-data="currentApplicationData"
|
||||
@success="handleApplicationSuccess"
|
||||
/>
|
||||
|
||||
<!-- 收货详情弹窗 -->
|
||||
<ReceiptDetailModal
|
||||
v-model:visible="receiptDetailModalVisible"
|
||||
:receipt-data="currentReceiptData"
|
||||
/>
|
||||
|
||||
<!-- 支付详情弹窗 -->
|
||||
<PaymentDetailModal
|
||||
v-model:visible="paymentDetailModalVisible"
|
||||
:payment-data="currentPaymentData"
|
||||
/>
|
||||
|
||||
<!-- 收货弹窗 -->
|
||||
<ReceiptModal
|
||||
v-model:visible="receiptModalVisible"
|
||||
:equipment-data="currentReceiptData"
|
||||
@success="handleReceiptSuccess"
|
||||
/>
|
||||
|
||||
<!-- 支付弹窗 -->
|
||||
<PaymentModal
|
||||
v-model:visible="paymentModalVisible"
|
||||
:equipment-data="currentPaymentData"
|
||||
@success="handlePaymentSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -271,6 +345,10 @@ import message from '@arco-design/web-vue/es/message'
|
|||
import ProcurementModal from './components/ProcurementModal.vue'
|
||||
import ProcurementSearch from './components/ProcurementSearch.vue'
|
||||
import ProcurementApplicationModal from './components/ProcurementApplicationModal.vue'
|
||||
import ReceiptDetailModal from './components/ReceiptDetailModal.vue'
|
||||
import PaymentDetailModal from './components/PaymentDetailModal.vue'
|
||||
import ReceiptModal from './components/ReceiptModal.vue'
|
||||
import PaymentModal from './components/PaymentModal.vue'
|
||||
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
|
||||
import { equipmentApprovalApi } from '@/apis/equipment/approval'
|
||||
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
|
||||
|
@ -307,6 +385,18 @@ const modalMode = ref<'add' | 'edit' | 'view'>('add')
|
|||
const applicationModalVisible = ref(false)
|
||||
const currentApplicationData = ref<EquipmentResp | null>(null)
|
||||
|
||||
// 收货详情弹窗控制
|
||||
const receiptDetailModalVisible = ref(false)
|
||||
const currentReceiptData = ref<EquipmentResp | null>(null)
|
||||
|
||||
// 支付详情弹窗控制
|
||||
const paymentDetailModalVisible = ref(false)
|
||||
const currentPaymentData = ref<EquipmentResp | null>(null)
|
||||
|
||||
// 收货弹窗控制
|
||||
const receiptModalVisible = ref(false)
|
||||
const paymentModalVisible = ref(false)
|
||||
|
||||
// 表格选择
|
||||
const selectedRowKeys = ref<string[]>([])
|
||||
const rowSelection = reactive({
|
||||
|
@ -418,6 +508,20 @@ const columns = [
|
|||
slotName: 'createTime',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: '收货状态',
|
||||
dataIndex: 'receiptStatus',
|
||||
key: 'receiptStatus',
|
||||
slotName: 'receiptStatus',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '支付状态',
|
||||
dataIndex: 'paymentStatus',
|
||||
key: 'paymentStatus',
|
||||
slotName: 'paymentStatus',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
|
@ -527,6 +631,50 @@ const getHealthStatusText = (status: string) => {
|
|||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取收货状态颜色
|
||||
const getReceiptStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
NOT_RECEIVED: 'gray',
|
||||
RECEIVED: 'green',
|
||||
PARTIALLY_RECEIVED: 'orange',
|
||||
REJECTED: 'red',
|
||||
}
|
||||
return colorMap[status] || 'blue'
|
||||
}
|
||||
|
||||
// 获取收货状态文本
|
||||
const getReceiptStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
NOT_RECEIVED: '未收货',
|
||||
RECEIVED: '已收货',
|
||||
PARTIALLY_RECEIVED: '部分收货',
|
||||
REJECTED: '已拒收',
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取支付状态颜色
|
||||
const getPaymentStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
NOT_PAID: 'gray',
|
||||
PAID: 'green',
|
||||
PARTIALLY_PAID: 'orange',
|
||||
REJECTED: 'red',
|
||||
}
|
||||
return colorMap[status] || 'blue'
|
||||
}
|
||||
|
||||
// 获取支付状态文本
|
||||
const getPaymentStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
NOT_PAID: '未支付',
|
||||
PAID: '已支付',
|
||||
PARTIALLY_PAID: '部分支付',
|
||||
REJECTED: '已拒付',
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 格式化价格
|
||||
const formatPrice = (price: number) => {
|
||||
return price.toLocaleString('zh-CN', {
|
||||
|
@ -601,6 +749,8 @@ const transformBackendData = (data: any[]): EquipmentResp[] => {
|
|||
inventoryBasis: item.inventoryBasis,
|
||||
dynamicRecord: item.dynamicRecord,
|
||||
procurementStatus: item.procurementStatus,
|
||||
receiptStatus: item.receiptStatus || 'NOT_RECEIVED',
|
||||
paymentStatus: item.paymentStatus || 'NOT_PAID',
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -850,6 +1000,54 @@ const canApplyProcurement = (record: EquipmentResp) => {
|
|||
return canApply
|
||||
}
|
||||
|
||||
// 检查是否可以收货
|
||||
const canReceiveGoods = (record: EquipmentResp) => {
|
||||
const receiptStatus = (record as any).receiptStatus
|
||||
return receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED'
|
||||
}
|
||||
|
||||
// 收货操作
|
||||
const handleReceiveGoods = async (record: EquipmentResp) => {
|
||||
currentReceiptData.value = { ...record }
|
||||
receiptModalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看收货详情
|
||||
const handleViewReceipt = (record: EquipmentResp) => {
|
||||
currentReceiptData.value = { ...record }
|
||||
receiptDetailModalVisible.value = true
|
||||
}
|
||||
|
||||
// 收货成功回调
|
||||
const handleReceiptSuccess = () => {
|
||||
receiptModalVisible.value = false
|
||||
loadData(currentSearchParams.value)
|
||||
}
|
||||
|
||||
// 检查是否可以付款
|
||||
const canMakePayment = (record: EquipmentResp) => {
|
||||
const paymentStatus = (record as any).paymentStatus
|
||||
return paymentStatus === 'NOT_PAID' || paymentStatus === 'PARTIALLY_PAID'
|
||||
}
|
||||
|
||||
// 付款操作
|
||||
const handleMakePayment = async (record: EquipmentResp) => {
|
||||
currentPaymentData.value = { ...record }
|
||||
paymentModalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看支付详情
|
||||
const handleViewPayment = (record: EquipmentResp) => {
|
||||
currentPaymentData.value = { ...record }
|
||||
paymentDetailModalVisible.value = true
|
||||
}
|
||||
|
||||
// 支付成功回调
|
||||
const handlePaymentSuccess = () => {
|
||||
paymentModalVisible.value = false
|
||||
loadData(currentSearchParams.value)
|
||||
}
|
||||
|
||||
// 获取审批状态颜色
|
||||
const getApprovalStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<p class="page-description">审批转交的任务</p>
|
||||
</div>
|
||||
|
||||
<!-- 状态汇总,改为左边文字、右边数字布局 -->
|
||||
<!-- 状态汇总,改为左边文字、右边数字布局。 -->
|
||||
<div class="status-summary">
|
||||
<div
|
||||
class="status-item pending"
|
||||
|
@ -200,47 +200,51 @@ const handleReject = (taskId: string) => {
|
|||
|
||||
/* 待审批状态样式 */
|
||||
.status-item.pending {
|
||||
border-top-color: #f59e0b;
|
||||
background-color: rgba(245, 158, 11, 0.1); /* 浅橙色背景 */
|
||||
background-color: #f59e0b;
|
||||
background-image: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
}
|
||||
.status-item.pending .count {
|
||||
color: #f59e0b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 已通过状态样式 */
|
||||
.status-item.approved {
|
||||
border-top-color: #10b981;
|
||||
background-color: rgba(16, 185, 129, 0.1); /* 浅绿色背景 */
|
||||
background-color: #10b981;
|
||||
background-image: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
.status-item.approved .count {
|
||||
color: #10b981;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 已拒绝状态样式 */
|
||||
.status-item.rejected {
|
||||
border-top-color: #ef4444;
|
||||
background-color: rgba(239, 68, 68, 0.1); /* 浅红色背景 */
|
||||
background-color: #ef4444;
|
||||
background-image: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
}
|
||||
.status-item.rejected .count {
|
||||
color: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 状态文字样式 */
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 数字样式 */
|
||||
.count {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 激活态样式(可选,点击后高亮) */
|
||||
.status-item.active {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
|
@ -1,38 +1,711 @@
|
|||
<template>
|
||||
<GiPageLayout>
|
||||
<div class="task-progress-page">
|
||||
<div class="task-tracking-page">
|
||||
<!-- 固定标题和表头容器 -->
|
||||
<div class="sticky-headers">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2>任务跟踪</h2>
|
||||
<p class="page-description">跟踪、监控和评估任务的完成情况</p>
|
||||
</div>
|
||||
|
||||
<!-- 公共表头(仅显示一次) -->
|
||||
<div class="shared-header">
|
||||
<div class="header-row">
|
||||
<div class="col" style="width: 100px">任务描述</div>
|
||||
<div class="col" style="width: 180px">任务情况总结</div>
|
||||
<div class="col" style="width: 80px">任务执行人</div>
|
||||
<div class="col" style="width: 60px">进展</div>
|
||||
<div class="col" style="width: 120px">开始日期</div>
|
||||
<div class="col" style="width: 120px">预计完成日期</div>
|
||||
<div class="col" style="width: 80px">是否延期</div>
|
||||
<div class="col" style="width: 120px">实际完成日期</div>
|
||||
<div class="col" style="width: 180px">最新进展记录</div>
|
||||
<div class="col" style="width: 100px">重要紧急程度</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组容器:按重要紧急程度分组 -->
|
||||
<div
|
||||
v-for="(group, groupKey) in groupedTasks"
|
||||
:key="groupKey"
|
||||
class="task-group"
|
||||
>
|
||||
<!-- 分组标题(带专门的折叠/展开按钮) -->
|
||||
<div class="group-header">
|
||||
<span class="group-title-text" :class="groupKey">{{ groupKey }}</span>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
@click="toggleGroup(groupKey)"
|
||||
:aria-expanded="!group.collapsed"
|
||||
>
|
||||
<i class="icon" :class="group.collapsed ? 'el-icon-plus' : 'el-icon-minus'" />
|
||||
<span class="toggle-text">{{ group.collapsed ? '展开' : '收起' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表:折叠时隐藏,展开时显示 -->
|
||||
<div class="task-list" v-show="!group.collapsed">
|
||||
<div
|
||||
v-for="(task, index) in group.tasks"
|
||||
:key="index"
|
||||
class="task-row"
|
||||
>
|
||||
<!-- 任务描述 -->
|
||||
<div class="col" style="width: 100px">{{ task.taskDesc }}</div>
|
||||
<!-- 任务情况总结(带弹窗) -->
|
||||
<div class="col info-cell" style="width: 180px">
|
||||
<span @click="openPopup($event, task.summaryDetail, '任务情况总结')"
|
||||
@mouseenter="cancelClosePopup"
|
||||
@mouseleave="closePopup">
|
||||
{{ task.summary }}
|
||||
<i class="el-icon-info" />
|
||||
</span>
|
||||
</div>
|
||||
<!-- 任务执行人 -->
|
||||
<div class="col" style="width: 80px">{{ task.executor }}</div>
|
||||
<!-- 进展(标签化) -->
|
||||
<div class="col progress-tag" :class="task.progress" style="width: 60px">
|
||||
{{ task.progress }}
|
||||
</div>
|
||||
<!-- 开始日期 -->
|
||||
<div class="col" style="width: 120px">{{ task.startDate }}</div>
|
||||
<!-- 预计完成日期 -->
|
||||
<div class="col" style="width: 120px">{{ task.expectEndDate }}</div>
|
||||
<!-- 是否延期(标签化) -->
|
||||
<div class="col delay-tag" :class="task.isDelay" style="width: 80px">
|
||||
{{ task.isDelay }}
|
||||
</div>
|
||||
<!-- 实际完成日期 -->
|
||||
<div class="col" style="width: 120px">{{ task.actualEndDate }}</div>
|
||||
<!-- 最新进展记录(带弹窗) -->
|
||||
<div class="col info-cell" style="width: 180px">
|
||||
<span @click="openPopup($event, task.progressDetail, '最新进展记录')"
|
||||
@mouseenter="cancelClosePopup"
|
||||
@mouseleave="closePopup">
|
||||
{{ task.latestProgress }}
|
||||
<i class="el-icon-info" />
|
||||
</span>
|
||||
</div>
|
||||
<!-- 重要紧急程度(标签化) -->
|
||||
<div class="col priority-tag" :class="groupKey" style="width: 100px">
|
||||
{{ groupKey }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局弹窗:所有详情共用 -->
|
||||
<transition name="popup">
|
||||
<div
|
||||
class="popup"
|
||||
v-if="popupVisible"
|
||||
:style="{
|
||||
top: popupTop + 'px',
|
||||
left: popupLeft + 'px'
|
||||
}"
|
||||
@mouseenter="cancelClosePopup"
|
||||
@mouseleave="closePopup"
|
||||
@click.stop
|
||||
>
|
||||
<div class="popup-title">{{ popupTitle }}</div>
|
||||
<div class="popup-content">{{ popupContent }}</div>
|
||||
<div class="popup-arrow"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 定义任务结构
|
||||
interface Task {
|
||||
taskDesc: string;
|
||||
summary: string; // 任务情况总结(简略)
|
||||
summaryDetail: string; // 任务情况总结(详细)
|
||||
executor: string;
|
||||
progress: string; // 进展(如:进行中、已完成)
|
||||
priority: string; // 重要紧急程度(如:重要紧急、紧急不重要等)
|
||||
startDate: string;
|
||||
expectEndDate: string;
|
||||
isDelay: string; // 是否延期(如:已延期、正常)
|
||||
actualEndDate: string;
|
||||
latestProgress: string; // 最新进展(简略)
|
||||
progressDetail: string; // 最新进展(详细)
|
||||
}
|
||||
|
||||
// 定义分组结构
|
||||
interface TaskGroup {
|
||||
collapsed: boolean; // 是否折叠
|
||||
tasks: Task[]; // 该分组下的任务
|
||||
}
|
||||
|
||||
// 原始任务数据(模拟,实际从接口获取)
|
||||
const rawTasks = ref<Task[]>([
|
||||
{
|
||||
taskDesc: '完成年度财务报告',
|
||||
summary: '1. 任务执行人于小宁正...',
|
||||
summaryDetail: '任务执行人于小宁正按流程推进,已梳理数据框架,待最终核算。目前已完成资产负债表初步编制,利润表数据核对中,预计下周完成全部核算工作。',
|
||||
executor: '周北北',
|
||||
progress: '进行中',
|
||||
priority: '重要紧急',
|
||||
startDate: '2023/02/05',
|
||||
expectEndDate: '2024/11/25',
|
||||
isDelay: '已延期',
|
||||
actualEndDate: '',
|
||||
latestProgress: '已经收集了所有必要的财...',
|
||||
progressDetail: '已收集资产负债表、利润表原始数据,待合并现金流量表。本周重点完成了各部门费用核算,正在处理年末调整事项。'
|
||||
},
|
||||
{
|
||||
taskDesc: '更新公司官网内容',
|
||||
summary: '1. 正在收集各部门最新...',
|
||||
summaryDetail: '正在收集各部门最新资料,市场部和销售部已提交更新内容,技术部和人力资源部资料待收。预计下周一开始页面制作。',
|
||||
executor: '李小华',
|
||||
progress: '进行中',
|
||||
priority: '重要紧急',
|
||||
startDate: '2023/11/01',
|
||||
expectEndDate: '2023/11/30',
|
||||
isDelay: '正常',
|
||||
actualEndDate: '',
|
||||
latestProgress: '设计稿已确认,等待内容...',
|
||||
progressDetail: '设计稿已确认,等待各部门内容素材。目前已完成首页和产品页的设计,正在准备关于我们页面的素材。'
|
||||
},
|
||||
{
|
||||
taskDesc: '制定明年培训计划',
|
||||
summary: '1. 已完成需求调研,正...',
|
||||
summaryDetail: '已完成需求调研,正在整理各部门培训需求。调研显示,技术类和管理类培训需求最高,分别占比42%和35%。',
|
||||
executor: '张明明',
|
||||
progress: '进行中',
|
||||
priority: '重要不紧急',
|
||||
startDate: '2023/10/15',
|
||||
expectEndDate: '2023/12/15',
|
||||
isDelay: '正常',
|
||||
actualEndDate: '',
|
||||
latestProgress: '正在分析培训需求数据...',
|
||||
progressDetail: '正在分析培训需求数据,计划11月中旬完成初稿,11月底组织各部门负责人评审。'
|
||||
},
|
||||
{
|
||||
taskDesc: '组织年度员工团建活动',
|
||||
summary: '1. 任务已经完成,因特...',
|
||||
summaryDetail: '活动已落地执行,含团队协作游戏、主题分享环节,反馈良好。参与率达到95%,收集到23条有效反馈,其中85%为正面评价。',
|
||||
executor: '周北北',
|
||||
progress: '已完成',
|
||||
priority: '紧急不重要',
|
||||
startDate: '2023/01/18',
|
||||
expectEndDate: '2024/12/02',
|
||||
isDelay: '正常',
|
||||
actualEndDate: '2023/05/25',
|
||||
latestProgress: '已经确定了活动日期和地...',
|
||||
progressDetail: '选定XX营地,日期2023/05/20,含露营、烧烤、团队挑战。活动预算控制在计划内,实际花费比预算节省8%。'
|
||||
},
|
||||
{
|
||||
taskDesc: '办公室绿植更换',
|
||||
summary: '1. 已联系3家供应商,...',
|
||||
summaryDetail: '已联系3家供应商,正在比较报价和服务。现有绿植约60%需要更换,主要是走廊和公共区域的大型绿植。',
|
||||
executor: '王静静',
|
||||
progress: '待开始',
|
||||
priority: '不紧急不重要',
|
||||
startDate: '2023/11/20',
|
||||
expectEndDate: '2023/11/30',
|
||||
isDelay: '正常',
|
||||
actualEndDate: '',
|
||||
latestProgress: '正在筛选供应商,等待批...',
|
||||
progressDetail: '正在筛选供应商,等待审批。初步选定两家供应商,报价相差约15%,正在核实服务内容差异。'
|
||||
},
|
||||
{
|
||||
taskDesc: '更新员工通讯录',
|
||||
summary: '1. 收集各部门最新联...',
|
||||
summaryDetail: '正在收集各部门最新联系方式,已完成市场部和销售部的信息更新,技术部和人力资源部资料待收。',
|
||||
executor: '李小明',
|
||||
progress: '待开始',
|
||||
priority: '不紧急不重要',
|
||||
startDate: '2023/11/25',
|
||||
expectEndDate: '2023/12/15',
|
||||
isDelay: '正常',
|
||||
actualEndDate: '',
|
||||
latestProgress: '等待各部门提交最新联...',
|
||||
progressDetail: '已发送通知邮件给各部门负责人,要求提供最新员工联系方式,目前收到60%的回复。'
|
||||
},
|
||||
{
|
||||
taskDesc: '整理归档旧项目文档',
|
||||
summary: '1. 开始整理2022年...',
|
||||
summaryDetail: '开始整理2022年度已完成项目的文档,按照项目类型和日期进行分类归档,预计需要两周时间完成。',
|
||||
executor: '张小红',
|
||||
progress: '待开始',
|
||||
priority: '不紧急不重要',
|
||||
startDate: '2023/12/01',
|
||||
expectEndDate: '2023/12/15',
|
||||
isDelay: '正常',
|
||||
actualEndDate: '',
|
||||
latestProgress: '准备归档工具和分类标...',
|
||||
progressDetail: '已准备好归档所需的文件夹和标签,正在制定分类标准,等待主管审批。'
|
||||
}
|
||||
])
|
||||
|
||||
// 分组键(固定四个重要紧急程度)
|
||||
const groupKeys = ref(['重要紧急', '紧急不重要', '重要不紧急', '不紧急不重要'])
|
||||
|
||||
// 分组状态管理
|
||||
const groupCollapseState = ref<Record<string, boolean>>({})
|
||||
|
||||
// 构建分组数据:按重要紧急程度分组
|
||||
const groupedTasks = computed(() => {
|
||||
const groups: Record<string, TaskGroup> = {}
|
||||
|
||||
// 初始化分组
|
||||
groupKeys.value.forEach(key => {
|
||||
// 初始化折叠状态,如果已有状态则使用,否则默认展开(false)
|
||||
groupCollapseState.value[key] = groupCollapseState.value[key] ?? false
|
||||
|
||||
groups[key] = {
|
||||
collapsed: groupCollapseState.value[key],
|
||||
tasks: []
|
||||
}
|
||||
})
|
||||
|
||||
// 将任务分配到对应分组
|
||||
rawTasks.value.forEach(task => {
|
||||
const groupKey = task.priority
|
||||
if (groups[groupKey]) {
|
||||
groups[groupKey].tasks.push(task)
|
||||
}
|
||||
})
|
||||
|
||||
return groups
|
||||
})
|
||||
|
||||
// 折叠/展开分组
|
||||
function toggleGroup(groupKey: string) {
|
||||
groupCollapseState.value[groupKey] = !groupCollapseState.value[groupKey]
|
||||
// 保存状态到本地存储
|
||||
localStorage.setItem('taskGroupCollapse', JSON.stringify(groupCollapseState.value))
|
||||
}
|
||||
|
||||
// 组件挂载时恢复折叠状态
|
||||
onMounted(() => {
|
||||
const savedState = localStorage.getItem('taskGroupCollapse')
|
||||
if (savedState) {
|
||||
groupCollapseState.value = JSON.parse(savedState)
|
||||
}
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const popupVisible = ref(false)
|
||||
const popupTitle = ref('')
|
||||
const popupContent = ref('')
|
||||
const popupTop = ref(0)
|
||||
const popupLeft = ref(0)
|
||||
let popupTimer: number | null = null
|
||||
|
||||
// 打开详情弹窗
|
||||
function openPopup(event: MouseEvent, content: string, title: string) {
|
||||
// 清除之前的关闭计时器
|
||||
if (popupTimer) {
|
||||
clearTimeout(popupTimer)
|
||||
popupTimer = null
|
||||
}
|
||||
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const rect = target.getBoundingClientRect()
|
||||
|
||||
popupVisible.value = true
|
||||
popupTitle.value = title
|
||||
popupContent.value = content
|
||||
|
||||
// 固定居中显示
|
||||
popupTop.value = window.scrollY + window.innerHeight / 2 - 100
|
||||
popupLeft.value = window.innerWidth / 2 - 150
|
||||
}
|
||||
|
||||
// 延迟关闭弹窗(防止鼠标移动时意外关闭)
|
||||
function closePopup() {
|
||||
popupTimer = setTimeout(() => {
|
||||
popupVisible.value = false
|
||||
popupTimer = null
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 取消延迟关闭
|
||||
function cancelClosePopup() {
|
||||
if (popupTimer) {
|
||||
clearTimeout(popupTimer)
|
||||
popupTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 点击页面其他区域关闭弹窗
|
||||
function handleDocumentClick(e: MouseEvent) {
|
||||
const popup = document.querySelector('.popup')
|
||||
const infoCells = document.querySelectorAll('.info-cell')
|
||||
|
||||
// 如果点击的不是弹窗也不是信息单元格,则关闭弹窗
|
||||
if (popup && !popup.contains(e.target as Node) &&
|
||||
!Array.from(infoCells).some(cell => cell.contains(e.target as Node))) {
|
||||
closePopup()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleDocumentClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleDocumentClick)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-progress-page {
|
||||
height: 100%;
|
||||
/* 页面基础样式 */
|
||||
.task-tracking-page {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 公共表头 */
|
||||
.shared-header {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #eee;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.header-row .col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border-right: 1px solid #eee;
|
||||
padding: 0 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-row .col:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 分组样式 */
|
||||
.task-group {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 分组标题(带折叠/展开按钮) */
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.group-title-text {
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.group-title-text.重要紧急 {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.group-title-text.紧急不重要 {
|
||||
background: #fd7e14;
|
||||
}
|
||||
|
||||
.group-title-text.重要不紧急 {
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.group-title-text.不紧急不重要 {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
/* 折叠/展开按钮 */
|
||||
.toggle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
background-color: #e9ecef;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background-color: #dee2e6;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.toggle-btn .icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toggle-text {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 任务列表(展开时显示) */
|
||||
.task-tracking-page {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sticky-headers {
|
||||
position: sticky;
|
||||
top: -20px; /* 向上移动消除空隙 */
|
||||
background: #fff;
|
||||
z-index: 10;
|
||||
padding: 20px 0 10px;
|
||||
margin-top: -20px; /* 消除外部空隙 */
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 20px 0 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.shared-header {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.task-tracking-page::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.task-tracking-page::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.task-tracking-page::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.task-tracking-page::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 任务行样式 */
|
||||
.task-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px 0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.task-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.task-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.task-row .col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border-right: 1px solid #eee;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.task-row .col:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* 信息单元格(带弹窗) */
|
||||
.info-cell {
|
||||
cursor: pointer;
|
||||
color: #1890ff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-cell:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info-cell .el-icon-info {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 进展标签样式 */
|
||||
.progress-tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-tag.进行中 {
|
||||
background: #ffc107;
|
||||
}
|
||||
|
||||
.progress-tag.已完成 {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.progress-tag.待开始 {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
/* 是否延期标签 */
|
||||
.delay-tag {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.delay-tag.正常 {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.delay-tag.已延期 {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
/* 重要紧急程度标签 */
|
||||
.priority-tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.priority-tag.重要紧急 {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.priority-tag.紧急不重要 {
|
||||
background: #fd7e14;
|
||||
}
|
||||
|
||||
.priority-tag.重要不紧急 {
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.priority-tag.不紧急不重要 {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.popup {
|
||||
/* 定位到页面中间 */
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
max-width: 80vw;
|
||||
background: #fff;
|
||||
border: 1px solid #f70b0b;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 弹窗过渡动画 */
|
||||
.popup-enter-active {
|
||||
transition: all 0.15s ease-out;
|
||||
}
|
||||
|
||||
.popup-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
.popup-enter-to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
|
@ -173,6 +173,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import TaskForm from './components/TaskForm.vue';
|
||||
import AssigneeSelector from './components/AssigneeSelector.vue';
|
||||
const taskFormRef = ref<InstanceType<typeof TaskForm> | null>(null);
|
||||
|
@ -330,15 +331,15 @@ const resetSearch = () => {
|
|||
|
||||
const handleSubmit = () => {
|
||||
if (!taskFormRef.value?.form.taskName) {
|
||||
alert('请填写任务名称');
|
||||
Message.error('请填写任务名称');
|
||||
return;
|
||||
}
|
||||
if (!taskFormRef.value?.form.dueDate) {
|
||||
alert('请设置截止日期');
|
||||
Message.error('请设置截止日期');
|
||||
return;
|
||||
}
|
||||
if (!assigneeRef.value?.assignees.leader) {
|
||||
alert('请选择任务负责人');
|
||||
Message.error('请选择任务负责人');
|
||||
return;
|
||||
}
|
||||
const taskData = {
|
||||
|
@ -361,7 +362,7 @@ const handleSubmit = () => {
|
|||
priority: taskData.priority || 'medium'
|
||||
});
|
||||
console.log('发布任务数据:', taskData);
|
||||
alert('任务发布成功!');
|
||||
Message.success('任务发布成功!');
|
||||
showPublishModal.value = false;
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -56,7 +56,7 @@ const filteredUsers = ref<typeof users.value>([]);
|
|||
const filterUsersByDepartment = () => {
|
||||
if (!selectedDepartment.value) {
|
||||
filteredUsers.value = [];
|
||||
assignees.value.leader = '';
|
||||
assignees.value.leader = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ const filterUsersByDepartment = () => {
|
|||
|
||||
// 如果当前选择的负责人不在筛选后的列表中,则清空选择
|
||||
if (assignees.value.leader && !filteredUsers.value.some(u => u.id === assignees.value.leader)) {
|
||||
assignees.value.leader = '';
|
||||
assignees.value.leader = 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -80,7 +80,7 @@ const departments = ref([
|
|||
|
||||
// 分配数据
|
||||
const assignees = ref({
|
||||
leader: ''
|
||||
leader: 0
|
||||
});
|
||||
|
||||
// 暴露分配数据给父组件
|
||||
|
|
|
@ -119,6 +119,34 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-form {
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
/* 原有样式保持不变,新增以下样式 */
|
||||
.error-message {
|
||||
color: #ef4444;
|
||||
|
|
|
@ -0,0 +1,498 @@
|
|||
<template>
|
||||
|
||||
<GiPageLayout>
|
||||
<div class="raw-data-container">
|
||||
<!-- 顶部按钮 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="openUploadModal">
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传视频
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-form :model="filterForm" layout="inline">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
|
||||
allow-clear :disabled="!filterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
|
||||
:scroll="{ y: 'calc(100vh - 380px)' }">
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.preTreatment ? 'green' : 'red'">
|
||||
{{ record.preTreatment ? '已处理' : '未处理' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
|
||||
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
|
||||
<a-button size="mini" status="danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 上传弹窗 -->
|
||||
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
|
||||
@ok="handleUpload" @cancel="showUploadModal = false">
|
||||
<a-form :model="uploadForm" layout="vertical">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="onProjectChangeUpload" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
|
||||
allow-clear :disabled="!uploadForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" required>
|
||||
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="视频文件" required>
|
||||
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
|
||||
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
|
||||
list-type="picture-card" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model="uploadMode" type="button">
|
||||
<a-radio value="single">单文件</a-radio>
|
||||
<a-radio value="batch">批量</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</GiPageLayout>
|
||||
<!-- 视频预览弹窗 -->
|
||||
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||
@cancel="previewVisible = false">
|
||||
<a-tabs v-model:active-key="activePreviewTab" @change="activePreviewTab = $event as any">
|
||||
<!-- 原始视频 -->
|
||||
<a-tab-pane key="video" title="原始视频">
|
||||
<video v-if="previewUrl" :src="previewUrl" controls
|
||||
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
<a-tab-pane key="result" title="处理结果">
|
||||
<a-spin :loading="loadingResult">
|
||||
<a-space direction="vertical" size="medium" style="width: 100%">
|
||||
<!-- 图片 -->
|
||||
<img v-if="resultImgUrl" :src="resultImgUrl" style="max-width: 100%; border-radius: 4px"
|
||||
alt="last frame" />
|
||||
|
||||
<!-- JSON 预览 -->
|
||||
<a-card title="results.json" size="small">
|
||||
<pre>{{ JSON.stringify(resultJson, null, 2) }}</pre>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { IconUpload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList
|
||||
} from '@/apis/industrial-image'
|
||||
import {
|
||||
getVideoPage,
|
||||
uploadBatchVideo,
|
||||
uploadSingleVideo,
|
||||
deleteVideo,
|
||||
downloadVideo
|
||||
} from '@/apis/video-monitor'
|
||||
|
||||
/* ---------------- 下拉 & 表单 ---------------- */
|
||||
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptions = ref<{ label: string; value: string }[]>([]) // 筛选用
|
||||
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([]) // 上传弹窗用
|
||||
const typeOptions = [
|
||||
{ label: '净空', value: 'clearance' },
|
||||
{ label: '形变', value: 'deformation' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
|
||||
const uploadForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: '',
|
||||
type: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
|
||||
const uploadMode = ref<'single' | 'batch'>('single')
|
||||
|
||||
/* ---------------- 列表 ---------------- */
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||
// { title: '项目', dataIndex: 'projectName' },
|
||||
// { title: '机组', dataIndex: 'turbineName' },
|
||||
{ title: '类型', slotName: 'type' },
|
||||
{ title: '上传时间', dataIndex: 'uploadTime' },
|
||||
{ title: '状态', slotName: 'status' },
|
||||
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
|
||||
|
||||
/* ---------------- 控制弹窗 ---------------- */
|
||||
const showUploadModal = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
/* ---------------- 初始化 ---------------- */
|
||||
onMounted(async () => {
|
||||
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||
projectOptions.value = data.map((p: any) => ({ label: p.projectName, value: p.projectId }))
|
||||
handleQuery()
|
||||
})
|
||||
const activePreviewTab = ref<'video' | 'result'>('video') // 当前标签页
|
||||
const resultImgUrl = ref('')
|
||||
const resultJson = ref<Record<string, any>>({})
|
||||
const loadingResult = ref(false)
|
||||
const resutlVideoUrl = ref('')
|
||||
async function loadResultFiles(row: any) {
|
||||
if (!row.preTreatment) return
|
||||
loadingResult.value = true
|
||||
try {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
// 图片
|
||||
resultImgUrl.value = `${base}${row.preImagePath}/last_frame.jpg`
|
||||
resutlVideoUrl.value = `${base}${row.preImagePath}/annotated_video.mp4`
|
||||
// JSON
|
||||
const jsonUrl = `${base}${row.preImagePath}/results.json`
|
||||
const res = await fetch(jsonUrl)
|
||||
resultJson.value = await res.json()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
resultJson.value = {}
|
||||
} finally {
|
||||
loadingResult.value = false
|
||||
}
|
||||
console.log('result', resultImgUrl.value)
|
||||
}
|
||||
/* 项目 -> 机组(筛选) */
|
||||
watch(
|
||||
() => filterForm.projectId,
|
||||
async (val) => {
|
||||
filterForm.turbineId = ''
|
||||
turbineOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineOptions.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
)
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
|
||||
function handlePreview(row: any) {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
|
||||
previewVisible.value = true
|
||||
activePreviewTab.value = 'video' // 默认先显示视频
|
||||
if (row.preTreatment) {
|
||||
loadResultFiles(row) // 预加载结果
|
||||
}
|
||||
}
|
||||
/* 项目 -> 机组(上传弹窗) */
|
||||
async function onProjectChangeUpload(projectId: string) {
|
||||
uploadForm.turbineId = ''
|
||||
turbineOptionsUpload.value = []
|
||||
if (!projectId) return
|
||||
const { data } = await getTurbineList({ projectId })
|
||||
turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
|
||||
/* ---------------- 查询 ---------------- */
|
||||
function handleQuery() {
|
||||
pagination.current = 1
|
||||
loadTable()
|
||||
}
|
||||
async function loadTable() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
projectId: filterForm.projectId,
|
||||
turbineId: filterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
console.log(data)
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 上传 ---------------- */
|
||||
function openUploadModal() {
|
||||
uploadForm.projectId = ''
|
||||
uploadForm.turbineId = ''
|
||||
uploadForm.type = ''
|
||||
uploadForm.fileList = []
|
||||
showUploadModal.value = true
|
||||
}
|
||||
async function handleUpload() {
|
||||
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
|
||||
Message.warning('请完整填写')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const files = uploadForm.fileList.map((f: any) => f.file)
|
||||
if (uploadMode.value === 'single') {
|
||||
await uploadSingleVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files[0]
|
||||
)
|
||||
} else {
|
||||
await uploadBatchVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files
|
||||
)
|
||||
}
|
||||
Message.success('上传成功')
|
||||
showUploadModal.value = false
|
||||
loadTable()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 下载 / 删除 ---------------- */
|
||||
async function handleDownload(row: any) {
|
||||
const url = await downloadVideo(row.videoId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
async function handleDelete(row: any) {
|
||||
await deleteVideo(row.videoId)
|
||||
Message.success('删除成功')
|
||||
loadTable()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.raw-data-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-sections {
|
||||
.project-section {
|
||||
margin-bottom: 32px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 20px;
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.project-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.units-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.unit-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 360px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.unit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.video-item {
|
||||
width: 100px;
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 4px;
|
||||
|
||||
.video-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-progress {
|
||||
margin-top: 8px;
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-meta-info {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,665 @@
|
|||
<template>
|
||||
<GiPageLayout>
|
||||
<div class="raw-data-container">
|
||||
<!-- 顶部页签 -->
|
||||
<a-tabs v-model:active-key="topTab" type="rounded" destroy-on-hide>
|
||||
<!-- 原始数据 -->
|
||||
<a-tab-pane key="data" title="原始数据">
|
||||
<!-- 顶部按钮 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="openUploadModal">
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传视频
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-form :model="filterForm" layout="inline">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
|
||||
allow-clear :disabled="!filterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
|
||||
:scroll="{ y: 'calc(100vh - 380px)' }">
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.preTreatment ? 'green' : 'red'">
|
||||
{{ record.preTreatment ? '已处理' : '未处理' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
|
||||
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
|
||||
<a-button size="mini" status="danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 净空报告 -->
|
||||
<a-tab-pane key="report" title="净空报告">
|
||||
<!-- 筛选 -->
|
||||
<a-form :model="reportFilterForm" layout="inline" style="margin-bottom: 16px">
|
||||
<a-form-item label="项目">
|
||||
<a-select v-model="reportFilterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="reportFilterForm.turbineId" placeholder="请选择机组"
|
||||
:options="turbineReportOptions" allow-clear :disabled="!reportFilterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="loadReportGrid">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- 网格 -->
|
||||
<a-row :gutter="[24, 24]">
|
||||
<a-col v-for="item in reportGrid" :key="item.videoId" :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<!-- 卡片内部 -->
|
||||
<a-card hoverable>
|
||||
<div @click="openReportDetail(item)" style="cursor: pointer">
|
||||
<video :src="item.videoUrl"
|
||||
style="width: 100%; height: 120px; object-fit: cover; border-radius: 4px" />
|
||||
<div class="video-name">{{ item.videoName }}</div>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="8" style="margin-top: 8px">
|
||||
<a-col :span="8">
|
||||
<div class="stat-label">最小</div>
|
||||
<div class="stat-value">{{ item.min }} m</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="stat-label">最大</div>
|
||||
<div class="stat-value">{{ item.max }} m</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="stat-label">平均</div>
|
||||
<div class="stat-value">{{ item.avg }} m</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 查看详情按钮 -->
|
||||
<a-button size="mini" style="margin-top: 8px; width: 100%"
|
||||
@click="openReportDetail(item)">
|
||||
查看详情
|
||||
</a-button>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增:报告详情模态框 -->
|
||||
<a-modal v-model:visible="reportDetailVisible" title="净空报告详情" width="900px" :footer="false"
|
||||
@cancel="reportDetailVisible = false">
|
||||
<a-tabs size="small">
|
||||
<a-tab-pane key="trend" title="趋势数据">
|
||||
<a-table :columns="trendColumns" :data="currentReportDetail?.trend" size="small"
|
||||
:scroll="{ y: 400 }" row-key="frame" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="json" title="原始 JSON">
|
||||
<pre style="max-height: 400px; overflow: auto">{{
|
||||
JSON.stringify(currentReportDetail?.rawJson, null, 2)
|
||||
}}</pre>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<!-- 上传弹窗(已去掉处理结果) -->
|
||||
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
|
||||
@ok="handleUpload" @cancel="showUploadModal = false">
|
||||
<a-form :model="uploadForm" layout="vertical">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="onProjectChangeUpload" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
|
||||
allow-clear :disabled="!uploadForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" required>
|
||||
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="视频文件" required>
|
||||
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
|
||||
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
|
||||
list-type="picture-card" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model="uploadMode" type="button">
|
||||
<a-radio value="single">单文件</a-radio>
|
||||
<a-radio value="batch">批量</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览弹窗(仅保留原始视频) -->
|
||||
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||
@cancel="previewVisible = false">
|
||||
<a-tabs v-model:active-key="activePreviewTab">
|
||||
<a-tab-pane key="video" title="原始视频">
|
||||
<video v-if="previewUrl" :src="previewUrl" controls
|
||||
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { IconUpload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList
|
||||
} from '@/apis/industrial-image'
|
||||
import {
|
||||
getVideoPage,
|
||||
uploadBatchVideo,
|
||||
uploadSingleVideo,
|
||||
deleteVideo,
|
||||
downloadVideo
|
||||
} from '@/apis/video-monitor'
|
||||
|
||||
/* ---------------- 下拉 & 表单 ---------------- */
|
||||
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([])
|
||||
const typeOptions = [
|
||||
{ label: '净空', value: 'clearance' },
|
||||
{ label: '形变', value: 'deformation' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
const uploadForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: '',
|
||||
type: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
const uploadMode = ref<'single' | 'batch'>('single')
|
||||
const reportDetailVisible = ref(false)
|
||||
const currentReportDetail = ref<ReportItem | null>(null)
|
||||
|
||||
function openReportDetail(item: ReportItem) {
|
||||
currentReportDetail.value = item
|
||||
reportDetailVisible.value = true
|
||||
}
|
||||
/* ---------------- 列表 ---------------- */
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||
{ title: '类型', slotName: 'type' },
|
||||
{ title: '上传时间', dataIndex: 'uploadTime' },
|
||||
{ title: '状态', slotName: 'status' },
|
||||
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
|
||||
|
||||
/* ---------------- 顶部 Tab ---------------- */
|
||||
const topTab = ref<'data' | 'report'>('data')
|
||||
|
||||
/* ---------------- 报告筛选 ---------------- */
|
||||
const reportFilterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
const turbineReportOptions = ref<{ label: string; value: string }[]>([])
|
||||
|
||||
watch(
|
||||
() => reportFilterForm.projectId,
|
||||
async (val) => {
|
||||
reportFilterForm.turbineId = ''
|
||||
turbineReportOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineReportOptions.value = data.map((t: any) => ({
|
||||
label: t.turbineName,
|
||||
value: t.turbineId
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
/* ---------------- 报告网格 ---------------- */
|
||||
interface ReportItem {
|
||||
videoId: string
|
||||
videoName: string
|
||||
videoUrl: string
|
||||
min: string
|
||||
max: string
|
||||
avg: string
|
||||
trend: any[]
|
||||
rawJson: any
|
||||
_activeKey?: string[]
|
||||
}
|
||||
const reportGrid = ref<ReportItem[]>([])
|
||||
|
||||
async function loadReportGrid() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: 1,
|
||||
pageSize: 1000,
|
||||
projectId: reportFilterForm.projectId,
|
||||
turbineId: reportFilterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
const rows = data.filter(
|
||||
(r: any) => r.type === 'clearance' && r.preTreatment
|
||||
)
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
|
||||
reportGrid.value = await Promise.all(
|
||||
rows.map(async (r: any) => {
|
||||
const jsonUrl = `${base}${r.preImagePath}/results.json`
|
||||
const json = await (await fetch(jsonUrl)).json()
|
||||
|
||||
const all = Object.values(json.clearance_history || {})
|
||||
.flat()
|
||||
.filter((v: any) => typeof v === 'number')
|
||||
const min = Math.min(...all)
|
||||
const max = Math.max(...all)
|
||||
const avg = all.reduce((a: number, b: number) => a + b, 0) / all.length
|
||||
|
||||
const { tip0, tip1, tip2 } = json.clearance_history || {}
|
||||
const len = Math.max(tip0?.length ?? 0, tip1?.length ?? 0, tip2?.length ?? 0)
|
||||
const trend = Array.from({ length: len }, (_, i) => ({
|
||||
frame: i + 1,
|
||||
tip0: tip0?.[i]?.toFixed(3) ?? '-',
|
||||
tip1: tip1?.[i]?.toFixed(3) ?? '-',
|
||||
tip2: tip2?.[i]?.toFixed(3) ?? '-'
|
||||
}))
|
||||
|
||||
return {
|
||||
videoId: r.videoId,
|
||||
videoName: r.videoName,
|
||||
videoUrl: new URL(r.videoPath.replace(/^\/+/, ''), base).href,
|
||||
min: min.toFixed(2),
|
||||
max: max.toFixed(2),
|
||||
avg: avg.toFixed(2),
|
||||
trend,
|
||||
rawJson: json,
|
||||
_activeKey: []
|
||||
}
|
||||
})
|
||||
)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDetail(item: ReportItem) {
|
||||
item._activeKey = item._activeKey?.length ? [] : ['detail']
|
||||
}
|
||||
|
||||
/* ---------------- 原列表逻辑 ---------------- */
|
||||
onMounted(async () => {
|
||||
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||
projectOptions.value = data.map((p: any) => ({
|
||||
label: p.projectName,
|
||||
value: p.projectId
|
||||
}))
|
||||
handleQuery()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => filterForm.projectId,
|
||||
async (val) => {
|
||||
filterForm.turbineId = ''
|
||||
turbineOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineOptions.value = data.map((t: any) => ({
|
||||
label: t.turbineName,
|
||||
value: t.turbineId
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
/* ---------------- 列表查询 ---------------- */
|
||||
function handleQuery() {
|
||||
pagination.current = 1
|
||||
loadTable()
|
||||
}
|
||||
async function loadTable() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
projectId: filterForm.projectId,
|
||||
turbineId: filterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 上传 ---------------- */
|
||||
const showUploadModal = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
function openUploadModal() {
|
||||
uploadForm.projectId = ''
|
||||
uploadForm.turbineId = ''
|
||||
uploadForm.type = ''
|
||||
uploadForm.fileList = []
|
||||
showUploadModal.value = true
|
||||
}
|
||||
|
||||
async function onProjectChangeUpload(projectId: string) {
|
||||
uploadForm.turbineId = ''
|
||||
turbineOptionsUpload.value = []
|
||||
if (!projectId) return
|
||||
const { data } = await getTurbineList({ projectId })
|
||||
turbineOptionsUpload.value = data.map((t: any) => ({
|
||||
label: t.turbineName,
|
||||
value: t.turbineId
|
||||
}))
|
||||
}
|
||||
|
||||
async function handleUpload() {
|
||||
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
|
||||
Message.warning('请完整填写')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const files = uploadForm.fileList.map((f: any) => f.file)
|
||||
if (uploadMode.value === 'single') {
|
||||
await uploadSingleVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files[0]
|
||||
)
|
||||
} else {
|
||||
await uploadBatchVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files
|
||||
)
|
||||
}
|
||||
Message.success('上传成功')
|
||||
showUploadModal.value = false
|
||||
loadTable()
|
||||
// 如果当前在报告页,顺带刷新
|
||||
if (topTab.value === 'report') await loadReportGrid()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 下载 / 删除 ---------------- */
|
||||
async function handleDownload(row: any) {
|
||||
const url = await downloadVideo(row.videoId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
async function handleDelete(row: any) {
|
||||
await deleteVideo(row.videoId)
|
||||
Message.success('删除成功')
|
||||
loadTable()
|
||||
if (topTab.value === 'report') await loadReportGrid()
|
||||
}
|
||||
|
||||
/* ---------------- 预览 ---------------- */
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const activePreviewTab = ref<'video' | 'result'>('video')
|
||||
|
||||
function handlePreview(row: any) {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
|
||||
previewVisible.value = true
|
||||
activePreviewTab.value = 'video'
|
||||
}
|
||||
|
||||
/* ---------------- 趋势表格列 ---------------- */
|
||||
const trendColumns = [
|
||||
{ title: '帧', dataIndex: 'frame', width: 70 },
|
||||
{ title: 'tip0 (m)', dataIndex: 'tip0' },
|
||||
{ title: 'tip1 (m)', dataIndex: 'tip1' },
|
||||
{ title: 'tip2 (m)', dataIndex: 'tip2' }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.raw-data-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-sections {
|
||||
.project-section {
|
||||
margin-bottom: 32px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 20px;
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.project-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.units-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.unit-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 360px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.unit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.video-item {
|
||||
width: 100px;
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 4px;
|
||||
|
||||
.video-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-progress {
|
||||
margin-top: 8px;
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-meta-info {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.video-name {
|
||||
margin-top: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,497 @@
|
|||
<template>
|
||||
<GiPageLayout>
|
||||
<div class="raw-data-container">
|
||||
<!-- 顶部按钮 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="openUploadModal">
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传视频
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-form :model="filterForm" layout="inline">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
|
||||
allow-clear :disabled="!filterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
|
||||
:scroll="{ y: 'calc(100vh - 380px)' }">
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.preTreatment ? 'green' : 'red'">
|
||||
{{ record.preTreatment ? '已处理' : '未处理' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
|
||||
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
|
||||
<a-button size="mini" status="danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 上传弹窗 -->
|
||||
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
|
||||
@ok="handleUpload" @cancel="showUploadModal = false">
|
||||
<a-form :model="uploadForm" layout="vertical">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="onProjectChangeUpload" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
|
||||
allow-clear :disabled="!uploadForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" required>
|
||||
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="视频文件" required>
|
||||
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
|
||||
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
|
||||
list-type="picture-card" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model="uploadMode" type="button">
|
||||
<a-radio value="single">单文件</a-radio>
|
||||
<a-radio value="batch">批量</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</GiPageLayout>
|
||||
<!-- 视频预览弹窗 -->
|
||||
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||
@cancel="previewVisible = false">
|
||||
<a-tabs v-model:active-key="activePreviewTab" @change="activePreviewTab = $event as any">
|
||||
<!-- 原始视频 -->
|
||||
<a-tab-pane key="video" title="原始视频">
|
||||
<video v-if="previewUrl" :src="previewUrl" controls
|
||||
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
<a-tab-pane key="result" title="处理结果">
|
||||
<a-spin :loading="loadingResult">
|
||||
<a-space direction="vertical" size="medium" style="width: 100%">
|
||||
<!-- 图片 -->
|
||||
<img v-if="resultImgUrl" :src="resultImgUrl" style="max-width: 100%; border-radius: 4px"
|
||||
alt="last frame" />
|
||||
|
||||
<!-- JSON 预览 -->
|
||||
<a-card title="results.json" size="small">
|
||||
<pre>{{ JSON.stringify(resultJson, null, 2) }}</pre>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { IconUpload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList
|
||||
} from '@/apis/industrial-image'
|
||||
import {
|
||||
getVideoPage,
|
||||
uploadBatchVideo,
|
||||
uploadSingleVideo,
|
||||
deleteVideo,
|
||||
downloadVideo
|
||||
} from '@/apis/video-monitor'
|
||||
|
||||
/* ---------------- 下拉 & 表单 ---------------- */
|
||||
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptions = ref<{ label: string; value: string }[]>([]) // 筛选用
|
||||
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([]) // 上传弹窗用
|
||||
const typeOptions = [
|
||||
{ label: '净空', value: 'clearance' },
|
||||
{ label: '形变', value: 'deformation' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
|
||||
const uploadForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: '',
|
||||
type: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
|
||||
const uploadMode = ref<'single' | 'batch'>('single')
|
||||
|
||||
/* ---------------- 列表 ---------------- */
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||
// { title: '项目', dataIndex: 'projectName' },
|
||||
// { title: '机组', dataIndex: 'turbineName' },
|
||||
{ title: '类型', slotName: 'type' },
|
||||
{ title: '上传时间', dataIndex: 'uploadTime' },
|
||||
{ title: '状态', slotName: 'status' },
|
||||
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
|
||||
|
||||
/* ---------------- 控制弹窗 ---------------- */
|
||||
const showUploadModal = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
/* ---------------- 初始化 ---------------- */
|
||||
onMounted(async () => {
|
||||
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||
projectOptions.value = data.map((p: any) => ({ label: p.projectName, value: p.projectId }))
|
||||
handleQuery()
|
||||
})
|
||||
const activePreviewTab = ref<'video' | 'result'>('video') // 当前标签页
|
||||
const resultImgUrl = ref('')
|
||||
const resultJson = ref<Record<string, any>>({})
|
||||
const loadingResult = ref(false)
|
||||
|
||||
async function loadResultFiles(row: any) {
|
||||
if (!row.preTreatment) return
|
||||
loadingResult.value = true
|
||||
try {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
// 图片
|
||||
resultImgUrl.value = `${base}${row.preImagePath}/last_frame.jpg`
|
||||
|
||||
// JSON
|
||||
const jsonUrl = `${base}${row.preImagePath}/results.json`
|
||||
const res = await fetch(jsonUrl)
|
||||
resultJson.value = await res.json()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
resultJson.value = {}
|
||||
} finally {
|
||||
loadingResult.value = false
|
||||
}
|
||||
console.log('result', resultImgUrl.value)
|
||||
}
|
||||
/* 项目 -> 机组(筛选) */
|
||||
watch(
|
||||
() => filterForm.projectId,
|
||||
async (val) => {
|
||||
filterForm.turbineId = ''
|
||||
turbineOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineOptions.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
)
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
|
||||
function handlePreview(row: any) {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
|
||||
previewVisible.value = true
|
||||
activePreviewTab.value = 'video' // 默认先显示视频
|
||||
if (row.preTreatment) {
|
||||
loadResultFiles(row) // 预加载结果
|
||||
}
|
||||
}
|
||||
/* 项目 -> 机组(上传弹窗) */
|
||||
async function onProjectChangeUpload(projectId: string) {
|
||||
uploadForm.turbineId = ''
|
||||
turbineOptionsUpload.value = []
|
||||
if (!projectId) return
|
||||
const { data } = await getTurbineList({ projectId })
|
||||
turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
|
||||
/* ---------------- 查询 ---------------- */
|
||||
function handleQuery() {
|
||||
pagination.current = 1
|
||||
loadTable()
|
||||
}
|
||||
async function loadTable() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
projectId: filterForm.projectId,
|
||||
turbineId: filterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
console.log(data)
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 上传 ---------------- */
|
||||
function openUploadModal() {
|
||||
uploadForm.projectId = ''
|
||||
uploadForm.turbineId = ''
|
||||
uploadForm.type = ''
|
||||
uploadForm.fileList = []
|
||||
showUploadModal.value = true
|
||||
}
|
||||
async function handleUpload() {
|
||||
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
|
||||
Message.warning('请完整填写')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const files = uploadForm.fileList.map((f: any) => f.file)
|
||||
if (uploadMode.value === 'single') {
|
||||
await uploadSingleVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files[0]
|
||||
)
|
||||
} else {
|
||||
await uploadBatchVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files
|
||||
)
|
||||
}
|
||||
Message.success('上传成功')
|
||||
showUploadModal.value = false
|
||||
loadTable()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 下载 / 删除 ---------------- */
|
||||
async function handleDownload(row: any) {
|
||||
const url = await downloadVideo(row.videoId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
async function handleDelete(row: any) {
|
||||
await deleteVideo(row.videoId)
|
||||
Message.success('删除成功')
|
||||
loadTable()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.raw-data-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-sections {
|
||||
.project-section {
|
||||
margin-bottom: 32px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 20px;
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.project-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.units-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.unit-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 360px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.unit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.video-item {
|
||||
width: 100px;
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 4px;
|
||||
|
||||
.video-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-progress {
|
||||
margin-top: 8px;
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-meta-info {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,497 @@
|
|||
<template>
|
||||
<GiPageLayout>
|
||||
<div class="raw-data-container">
|
||||
<!-- 顶部按钮 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="openUploadModal">
|
||||
<template #icon>
|
||||
<IconUpload />
|
||||
</template>
|
||||
上传视频
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-form :model="filterForm" layout="inline">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
|
||||
allow-clear :disabled="!filterForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
|
||||
:scroll="{ y: 'calc(100vh - 380px)' }">
|
||||
<template #type="{ record }">
|
||||
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.preTreatment ? 'green' : 'red'">
|
||||
{{ record.preTreatment ? '已处理' : '未处理' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
|
||||
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
|
||||
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
|
||||
<a-button size="mini" status="danger">删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 上传弹窗 -->
|
||||
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
|
||||
@ok="handleUpload" @cancel="showUploadModal = false">
|
||||
<a-form :model="uploadForm" layout="vertical">
|
||||
<a-form-item label="项目" required>
|
||||
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||
allow-clear @change="onProjectChangeUpload" />
|
||||
</a-form-item>
|
||||
<a-form-item label="机组">
|
||||
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
|
||||
allow-clear :disabled="!uploadForm.projectId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="类型" required>
|
||||
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="视频文件" required>
|
||||
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
|
||||
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
|
||||
list-type="picture-card" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model="uploadMode" type="button">
|
||||
<a-radio value="single">单文件</a-radio>
|
||||
<a-radio value="batch">批量</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</GiPageLayout>
|
||||
<!-- 视频预览弹窗 -->
|
||||
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||
@cancel="previewVisible = false">
|
||||
<a-tabs v-model:active-key="activePreviewTab" @change="activePreviewTab = $event as any">
|
||||
<!-- 原始视频 -->
|
||||
<a-tab-pane key="video" title="原始视频">
|
||||
<video v-if="previewUrl" :src="previewUrl" controls
|
||||
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
<a-tab-pane key="result" title="处理结果">
|
||||
<a-spin :loading="loadingResult">
|
||||
<a-space direction="vertical" size="medium" style="width: 100%">
|
||||
<!-- 图片 -->
|
||||
<img v-if="resultImgUrl" :src="resultImgUrl" style="max-width: 100%; border-radius: 4px"
|
||||
alt="last frame" />
|
||||
|
||||
<!-- JSON 预览 -->
|
||||
<a-card title="results.json" size="small">
|
||||
<pre>{{ JSON.stringify(resultJson, null, 2) }}</pre>
|
||||
</a-card>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue'
|
||||
import { IconUpload } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
getProjectList,
|
||||
getTurbineList
|
||||
} from '@/apis/industrial-image'
|
||||
import {
|
||||
getVideoPage,
|
||||
uploadBatchVideo,
|
||||
uploadSingleVideo,
|
||||
deleteVideo,
|
||||
downloadVideo
|
||||
} from '@/apis/video-monitor'
|
||||
|
||||
/* ---------------- 下拉 & 表单 ---------------- */
|
||||
const projectOptions = ref<{ label: string; value: string }[]>([])
|
||||
const turbineOptions = ref<{ label: string; value: string }[]>([]) // 筛选用
|
||||
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([]) // 上传弹窗用
|
||||
const typeOptions = [
|
||||
{ label: '净空', value: 'clearance' },
|
||||
{ label: '形变', value: 'deformation' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: ''
|
||||
})
|
||||
|
||||
const uploadForm = reactive({
|
||||
projectId: '',
|
||||
turbineId: '',
|
||||
type: '',
|
||||
fileList: [] as any[]
|
||||
})
|
||||
|
||||
const uploadMode = ref<'single' | 'batch'>('single')
|
||||
|
||||
/* ---------------- 列表 ---------------- */
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||
// { title: '项目', dataIndex: 'projectName' },
|
||||
// { title: '机组', dataIndex: 'turbineName' },
|
||||
{ title: '类型', slotName: 'type' },
|
||||
{ title: '上传时间', dataIndex: 'uploadTime' },
|
||||
{ title: '状态', slotName: 'status' },
|
||||
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
|
||||
]
|
||||
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
|
||||
|
||||
/* ---------------- 控制弹窗 ---------------- */
|
||||
const showUploadModal = ref(false)
|
||||
const uploading = ref(false)
|
||||
|
||||
/* ---------------- 初始化 ---------------- */
|
||||
onMounted(async () => {
|
||||
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||
projectOptions.value = data.map((p: any) => ({ label: p.projectName, value: p.projectId }))
|
||||
handleQuery()
|
||||
})
|
||||
const activePreviewTab = ref<'video' | 'result'>('video') // 当前标签页
|
||||
const resultImgUrl = ref('')
|
||||
const resultJson = ref<Record<string, any>>({})
|
||||
const loadingResult = ref(false)
|
||||
|
||||
async function loadResultFiles(row: any) {
|
||||
if (!row.preTreatment) return
|
||||
loadingResult.value = true
|
||||
try {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
// 图片
|
||||
resultImgUrl.value = `${base}${row.preImagePath}/last_frame.jpg`
|
||||
|
||||
// JSON
|
||||
const jsonUrl = `${base}${row.preImagePath}/results.json`
|
||||
const res = await fetch(jsonUrl)
|
||||
resultJson.value = await res.json()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
resultJson.value = {}
|
||||
} finally {
|
||||
loadingResult.value = false
|
||||
}
|
||||
console.log('result', resultImgUrl.value)
|
||||
}
|
||||
/* 项目 -> 机组(筛选) */
|
||||
watch(
|
||||
() => filterForm.projectId,
|
||||
async (val) => {
|
||||
filterForm.turbineId = ''
|
||||
turbineOptions.value = []
|
||||
if (!val) return
|
||||
const { data } = await getTurbineList({ projectId: val })
|
||||
turbineOptions.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
)
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
|
||||
function handlePreview(row: any) {
|
||||
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
|
||||
previewVisible.value = true
|
||||
activePreviewTab.value = 'video' // 默认先显示视频
|
||||
if (row.preTreatment) {
|
||||
loadResultFiles(row) // 预加载结果
|
||||
}
|
||||
}
|
||||
/* 项目 -> 机组(上传弹窗) */
|
||||
async function onProjectChangeUpload(projectId: string) {
|
||||
uploadForm.turbineId = ''
|
||||
turbineOptionsUpload.value = []
|
||||
if (!projectId) return
|
||||
const { data } = await getTurbineList({ projectId })
|
||||
turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
|
||||
}
|
||||
|
||||
/* ---------------- 查询 ---------------- */
|
||||
function handleQuery() {
|
||||
pagination.current = 1
|
||||
loadTable()
|
||||
}
|
||||
async function loadTable() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
projectId: filterForm.projectId,
|
||||
turbineId: filterForm.turbineId || undefined
|
||||
}
|
||||
const { data } = await getVideoPage(params)
|
||||
console.log(data)
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 上传 ---------------- */
|
||||
function openUploadModal() {
|
||||
uploadForm.projectId = ''
|
||||
uploadForm.turbineId = ''
|
||||
uploadForm.type = ''
|
||||
uploadForm.fileList = []
|
||||
showUploadModal.value = true
|
||||
}
|
||||
async function handleUpload() {
|
||||
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
|
||||
Message.warning('请完整填写')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const files = uploadForm.fileList.map((f: any) => f.file)
|
||||
if (uploadMode.value === 'single') {
|
||||
await uploadSingleVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files[0]
|
||||
)
|
||||
} else {
|
||||
await uploadBatchVideo(
|
||||
uploadForm.projectId,
|
||||
uploadForm.turbineId || '',
|
||||
uploadForm.type,
|
||||
files
|
||||
)
|
||||
}
|
||||
Message.success('上传成功')
|
||||
showUploadModal.value = false
|
||||
loadTable()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 下载 / 删除 ---------------- */
|
||||
async function handleDownload(row: any) {
|
||||
const url = await downloadVideo(row.videoId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
async function handleDelete(row: any) {
|
||||
await deleteVideo(row.videoId)
|
||||
Message.success('删除成功')
|
||||
loadTable()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.raw-data-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.filter-section {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-sections {
|
||||
.project-section {
|
||||
margin-bottom: 32px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px #f0f1f2;
|
||||
padding: 20px;
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.project-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.units-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.unit-card {
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 360px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.unit-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.unit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-list {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.video-item {
|
||||
width: 100px;
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 4px;
|
||||
|
||||
.video-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-progress {
|
||||
margin-top: 8px;
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-meta-info {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
|
||||
p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue