This commit is contained in:
马诗敏 2025-08-14 14:41:59 +08:00
commit a3f30bf2d0
31 changed files with 6128 additions and 420 deletions

View File

@ -4,8 +4,8 @@ VITE_API_PREFIX = '/dev-api'
# 接口地址 # 接口地址
# VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/' # VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
# VITE_API_BASE_URL = 'http://localhost: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.163:8888/'
# VITE_API_BASE_URL = 'http://10.18.34.213:8888/' # VITE_API_BASE_URL = 'http://10.18.34.213:8888/'
# 接口地址 (WebSocket) # 接口地址 (WebSocket)

View File

@ -49,4 +49,24 @@ export function returnEquipment(equipmentId: string) {
} }
// 导出设备采购 API // 导出设备采购 API
export * from './procurement' 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 }
})
}

View File

@ -99,6 +99,37 @@ export const equipmentProcurementApi = {
return http.get<ApiRes<EquipmentResp>>(`/equipment/procurement/detail/${equipmentId}`) 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) => { export: (params: EquipmentListReq) => {
return http.get<Blob>('/equipment/procurement/export', { return http.get('/equipment/procurement/export', params, { responseType: 'blob' })
params,
responseType: 'blob'
})
} }
} }

View File

@ -101,28 +101,28 @@ export interface EquipmentResp {
/** 资产编号 */ /** 资产编号 */
assetCode?: string assetCode?: string
/** 设备名称 */ /** 设备名称 */
equipmentName: string equipmentName?: string
/** 设备类型 */ /** 设备类型 */
equipmentType: string equipmentType?: string
/** 设备类型描述 */ /** 设备类型标签 */
equipmentTypeLabel?: string equipmentTypeLabel?: string
/** 设备型号 */ /** 设备型号 */
equipmentModel: string equipmentModel?: string
/** 设备SN */ /** 设备序列号 */
equipmentSn: string equipmentSn?: string
/** 品牌 */ /** 品牌 */
brand?: string brand?: string
/** 配置规格/参数 */ /** 配置规格/参数 */
specification?: string specification?: string
/** 设备状态 */ /** 设备状态 */
equipmentStatus: string equipmentStatus?: string
/** 设备状态描述 */ /** 设备状态标签 */
equipmentStatusLabel?: string equipmentStatusLabel?: string
/** 使用状态 */ /** 使用状态 */
useStatus: string useStatus?: string
/** 位置状态 */ /** 位置状态 */
locationStatus?: string locationStatus?: string
/** 位置状态描述 */ /** 位置状态标签 */
locationStatusLabel?: string locationStatusLabel?: string
/** 设备当前物理位置 */ /** 设备当前物理位置 */
physicalLocation?: string physicalLocation?: string
@ -130,7 +130,7 @@ export interface EquipmentResp {
responsiblePerson?: string responsiblePerson?: string
/** 健康状态 */ /** 健康状态 */
healthStatus?: string healthStatus?: string
/** 健康状态描述 */ /** 健康状态标签 */
healthStatusLabel?: string healthStatusLabel?: string
/** 采购时间 */ /** 采购时间 */
purchaseTime?: string purchaseTime?: string
@ -138,13 +138,13 @@ export interface EquipmentResp {
inStockTime?: string inStockTime?: string
/** 启用时间 */ /** 启用时间 */
activationTime?: string activationTime?: string
/** 预报废时间 */ /** 预报废时间 */
expectedScrapTime?: string expectedScrapTime?: string
/** 实际报废时间 */ /** 实际报废时间 */
actualScrapTime?: string actualScrapTime?: string
/** 状态变更时间 */ /** 状态变更时间 */
statusChangeTime?: string statusChangeTime?: string
/** 采购订单 */ /** 采购订单 */
purchaseOrder?: string purchaseOrder?: string
/** 供应商名称 */ /** 供应商名称 */
supplierName?: string supplierName?: string
@ -158,9 +158,9 @@ export interface EquipmentResp {
depreciationYears?: number depreciationYears?: number
/** 残值 */ /** 残值 */
salvageValue?: number salvageValue?: number
/** 保修截止日期 */ /** 保修到期日期 */
warrantyExpireDate?: string warrantyExpireDate?: string
/** 上次维护日期 */ /** 最后维护日期 */
lastMaintenanceDate?: string lastMaintenanceDate?: string
/** 下次维护日期 */ /** 下次维护日期 */
nextMaintenanceDate?: string nextMaintenanceDate?: string
@ -176,7 +176,7 @@ export interface EquipmentResp {
projectName?: string projectName?: string
/** 使用人ID */ /** 使用人ID */
userId?: string userId?: string
/** 使用人 */ /** 使用人姓名 */
name?: string name?: string
/** 创建时间 */ /** 创建时间 */
createTime?: string createTime?: string
@ -194,12 +194,14 @@ export interface EquipmentResp {
inventoryBasis?: string inventoryBasis?: string
/** 动态记录 */ /** 动态记录 */
dynamicRecord?: string dynamicRecord?: string
/** 采购状态 */ /** 采购状态 */
procurementStatus?: string procurementStatus?: string
/** 审批状态 */ /** 审批状态 */
approvalStatus?: string approvalStatus?: string
/** 收货状态 */
receiptStatus?: string
/** 支付状态 */
paymentStatus?: string
} }
/** /**
@ -406,3 +408,221 @@ export interface EquipmentApprovalResp {
/** 更新时间 */ /** 更新时间 */
updateTime: string 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
}

View File

@ -15,6 +15,12 @@ export function getProject(id: string | number) {
return http.get<T.ProjectResp>(`${BASE_URL}/${id}`) 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 新增项目 */ /** @desc 新增项目 */
export function addProject(data: any) { export function addProject(data: any) {
return http.post(`${BASE_URL}`, data) return http.post(`${BASE_URL}`, data)
@ -49,4 +55,4 @@ export function importProject(file: File) {
/** @desc 导出项目 */ /** @desc 导出项目 */
export function exportProject(query: T.ProjectQuery) { export function exportProject(query: T.ProjectQuery) {
return http.download(`${BASE_URL}/export`, query) return http.download(`${BASE_URL}/export`, query)
} }

View File

@ -18,6 +18,8 @@ export interface ProjectResp {
projectCategory?: string // 项目类型/服务 projectCategory?: string // 项目类型/服务
projectManagerId?: string // 项目经理ID projectManagerId?: string // 项目经理ID
projectManagerName?: string // 项目经理姓名 projectManagerName?: string // 项目经理姓名
projectOrigin?: string // 项目来源
projectStaff?: string[] // 施工人员 projectStaff?: string[] // 施工人员
startDate?: string // 开始日期 startDate?: string // 开始日期
endDate?: string // 结束日期 endDate?: string // 结束日期
@ -28,10 +30,10 @@ export interface ProjectResp {
coverUrl?: string // 封面URL coverUrl?: string // 封面URL
createDt?: Date createDt?: Date
updateDt?: Date updateDt?: Date
// 为了保持向后兼容,添加一些别名字段 // 为了保持向后兼容,添加一些别名字段
id?: string // projectId的别名 id?: string // projectId的别名
fieldName?: string // farmName的别名 fieldName?: string // farmName的别名
fieldLocation?: string // farmAddress的别名 fieldLocation?: string // farmAddress的别名
commissionUnit?: string // client的别名 commissionUnit?: string // client的别名
commissionContact?: string // clientContact的别名 commissionContact?: string // clientContact的别名
@ -85,7 +87,7 @@ export interface TaskQuery {
status?: string status?: string
} }
export interface TaskPageQuery extends TaskQuery, PageQuery {} export interface TaskPageQuery extends TaskQuery, PageQuery {}
// ==================== 人员调度相关类型 ==================== // ==================== 人员调度相关类型 ====================
@ -339,4 +341,4 @@ export interface PageRes<T> {
total: number total: number
page: number page: number
pageSize: number pageSize: number
} }

View File

@ -19,7 +19,7 @@
:mask-closable="true" :mask-closable="true"
:closable="true" :closable="true"
:destroy-on-close="false" :destroy-on-close="false"
:z-index="999999" :z-index="1000"
class="notification-modal" class="notification-modal"
> >
<!-- 消息中心头部 --> <!-- 消息中心头部 -->
@ -673,7 +673,7 @@ defineExpose({
<style scoped lang="scss"> <style scoped lang="scss">
.notification-center { .notification-center {
position: relative; position: relative;
z-index: 999999; z-index: 1000;
.notification-trigger { .notification-trigger {
cursor: pointer; cursor: pointer;
@ -699,15 +699,15 @@ defineExpose({
// //
.notification-modal { .notification-modal {
:deep(.arco-modal) { :deep(.arco-modal) {
z-index: 999999 !important; z-index: 1000 !important;
} }
:deep(.arco-modal-mask) { :deep(.arco-modal-mask) {
z-index: 999998 !important; z-index: 999 !important;
} }
:deep(.arco-modal-wrapper) { :deep(.arco-modal-wrapper) {
z-index: 999999 !important; z-index: 1000 !important;
} }
} }
@ -858,37 +858,37 @@ defineExpose({
padding: 16px; padding: 16px;
} }
// //
:deep(.arco-modal) { :deep(.arco-modal) {
z-index: 999999 !important; z-index: 1000 !important;
} }
:deep(.arco-modal-mask) { :deep(.arco-modal-mask) {
z-index: 999998 !important; z-index: 999 !important;
} }
:deep(.arco-modal-wrapper) { :deep(.arco-modal-wrapper) {
z-index: 999999 !important; z-index: 1000 !important;
} }
// Arco Design v2 // Arco Design v2
:deep(.arco-overlay) { :deep(.arco-overlay) {
z-index: 999999 !important; z-index: 1000 !important;
} }
:deep(.arco-overlay-container) { :deep(.arco-overlay-container) {
z-index: 999999 !important; z-index: 1000 !important;
} }
// //
:deep(.arco-modal) { :deep(.arco-modal) {
z-index: 999999 !important; z-index: 1000 !important;
position: relative !important; position: relative !important;
} }
// //
:deep(.arco-modal-wrapper) { :deep(.arco-modal-wrapper) {
z-index: 999999 !important; z-index: 1000 !important;
position: relative !important; position: relative !important;
} }
</style> </style>

View File

@ -1199,46 +1199,141 @@ export const systemRoutes: RouteRecordRaw[] = [
], ],
}, },
{ {
path: '/chat-platform', path: '/image-detection',
name: 'ChatPlatform', name: 'ImageDetection',
component: Layout, component: Layout,
redirect: '/chat-platform/options', redirect: '/Image-detection/tower-monitoring/clearance-monitoring',
meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 }, meta: {
title: '图像检测',
icon: 'monitor',
hidden: false,
sort: 6.5,
},
children: [ children: [
// { {
// path: '/chat-platform/options', path: '/image-detection/image-analysis',
// name: 'ChatOptions', name: 'ImageAnalysis',
// component: () => import('@/views/default/redirect/index.vue'), // 临时使用一个组件,实际开发中需要替换 component: () => import('@/components/ParentView/index.vue'),
// meta: { meta: {
// title: '二级选项1', title: '检查图像分析',
// icon: 'setting', icon: 'line-chart',
// hidden: false 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', // path: '/chat-platform',
// name: 'UserProfile', // name: 'ChatPlatform',
// component: Layout, // component: Layout,
// redirect: '/user/profile', // redirect: '/chat-platform/options',
// meta: { // meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
// title: '个人中心',
// icon: 'user',
// hidden: false,
// sort: 100,
// },
// children: [ // 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, 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', path: '/system-resource/device-management/online',
name: 'SystemResourceDeviceOnline', name: 'SystemResourceDeviceOnline',

View File

@ -226,6 +226,20 @@ const storeSetup = () => {
{ {
id: 2013, id: 2013,
parentId: 2010, 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: '审批台', title: '审批台',
type: 2, type: 2,
path: '/asset-management/device-management/approval', path: '/asset-management/device-management/approval',
@ -235,10 +249,10 @@ const storeSetup = () => {
isExternal: false, isExternal: false,
isCache: false, isCache: false,
isHidden: false, isHidden: false,
sort: 3, sort: 4,
}, },
{ {
id: 2014, id: 2015,
parentId: 2010, parentId: 2010,
title: '在线管理', title: '在线管理',
type: 1, type: 1,
@ -250,11 +264,11 @@ const storeSetup = () => {
isExternal: false, isExternal: false,
isCache: false, isCache: false,
isHidden: false, isHidden: false,
sort: 4, sort: 5,
children: [ children: [
{ {
id: 20141, id: 20151,
parentId: 2014, parentId: 2015,
title: '无人机', title: '无人机',
type: 2, type: 2,
path: '/asset-management/device-management/online/drone', path: '/asset-management/device-management/online/drone',
@ -267,8 +281,8 @@ const storeSetup = () => {
sort: 1, sort: 1,
}, },
{ {
id: 20142, id: 20152,
parentId: 2014, parentId: 2015,
title: '机巢', title: '机巢',
type: 2, type: 2,
path: '/asset-management/device-management/online/nest', path: '/asset-management/device-management/online/nest',
@ -281,8 +295,8 @@ const storeSetup = () => {
sort: 2, sort: 2,
}, },
{ {
id: 20143, id: 20153,
parentId: 2014, parentId: 2015,
title: '其他智能终端', title: '其他智能终端',
type: 2, type: 2,
path: '/asset-management/device-management/online/smart-terminal', path: '/asset-management/device-management/online/smart-terminal',
@ -297,7 +311,7 @@ const storeSetup = () => {
], ],
}, },
{ {
id: 2015, id: 2016,
parentId: 2010, parentId: 2010,
title: '设备详情', title: '设备详情',
type: 2, type: 2,
@ -308,7 +322,7 @@ const storeSetup = () => {
isExternal: false, isExternal: false,
isCache: false, isCache: false,
isHidden: true, isHidden: true,
sort: 5, sort: 6,
}, },
], ],
}, },

View File

@ -7,70 +7,7 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { 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'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] 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']
} }
} }

View File

@ -143,19 +143,22 @@
<a-layout-content class="file-content"> <a-layout-content class="file-content">
<a-card :bordered="false" class="file-card"> <a-card :bordered="false" class="file-card">
<a-descriptions :title="`文件列表 (${fileList.length})`" v-if="currentFolderId" /> <!-- 文件列表标题和搜索框在同一行 -->
<div v-if="currentFolderId" class="file-header-container">
<!-- 文件搜索功能 --> <div class="file-title">
<div v-if="currentFolderId" class="file-search-container"> <span class="file-list-title">文件列表 ({{ fileList.length }})</span>
<a-input-search </div>
v-model="fileSearchKeyword" <div class="file-search-container">
placeholder="搜索文件名..." <a-input-search
class="file-search-input" v-model="fileSearchKeyword"
@search="handleFileSearch" placeholder="搜索文件名..."
@input="handleFileSearchInput" class="file-search-input"
@clear="handleFileSearchClear" @search="handleFileSearch"
allow-clear @input="handleFileSearchInput"
/> @clear="handleFileSearchClear"
allow-clear
/>
</div>
</div> </div>
<a-divider size="small" v-if="currentFolderId" /> <a-divider size="small" v-if="currentFolderId" />
@ -336,7 +339,7 @@
</div> </div>
<!-- 文件分页 --> <!-- 文件分页 -->
<div v-if="currentFolderId && !loading && totalFiles > 0" class="file-pagination"> <div v-if="currentFolderId && !loading && totalFiles > 0" class="pagination-container">
<a-pagination <a-pagination
:total="totalFiles" :total="totalFiles"
:current="fileCurrentPage" :current="fileCurrentPage"
@ -345,6 +348,8 @@
:show-page-size="true" :show-page-size="true"
:page-size-options="[10, 20, 50, 100]" :page-size-options="[10, 20, 50, 100]"
:show-jumper="true" :show-jumper="true"
:hide-on-single-page="false"
size="default"
@change="handleFilePageChange" @change="handleFilePageChange"
@page-size-change="handleFilePageSizeChange" @page-size-change="handleFilePageSizeChange"
/> />
@ -2453,8 +2458,6 @@ onMounted(() => {
background-color: var(--color-bg-1); background-color: var(--color-bg-1);
} }
/* 侧边栏样式 */ /* 侧边栏样式 */
.folder-sidebar { .folder-sidebar {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
@ -2629,6 +2632,7 @@ onMounted(() => {
background: var(--color-bg-1); background: var(--color-bg-1);
min-height: 0; min-height: 0;
max-height: calc(100vh - 120px); max-height: calc(100vh - 120px);
position: relative;
} }
.file-card { .file-card {
@ -2640,6 +2644,7 @@ onMounted(() => {
position: relative; position: relative;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
padding-bottom: 80px; /* 为分页器留出空间 */
} }
/* 表格容器 */ /* 表格容器 */
@ -2654,7 +2659,7 @@ onMounted(() => {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 0; margin-bottom: 0;
min-height: 300px; min-height: 300px;
max-height: calc(100vh - 300px); max-height: calc(100vh - 380px); /* 调整高度为分页器留出空间 */
} }
/* 表头行样式 */ /* 表头行样式 */
@ -3100,14 +3105,65 @@ onMounted(() => {
} }
/* 分页样式 */ /* 分页样式 */
.pagination-container, .file-pagination { .pagination-container {
margin-top: 16px; position: absolute;
text-align: right; bottom: 0;
padding: 0 16px 16px; left: 0;
} right: 0;
background: var(--color-bg-1);
.file-pagination { padding: 16px 24px;
border-top: 1px solid var(--color-border); 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%); } 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 { .file-search-container {
margin: 16px 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
@ -3601,55 +3677,7 @@ onMounted(() => {
border-top-color: var(--color-primary); 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 { .folder-tree-container {

View File

@ -73,18 +73,8 @@
</div> </div>
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="props" tap="形变" title="形变原数据">
<div class="tab-content">
<raw-data>
</raw-data>
</div>
</a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
<!-- 文件预览模态框待重新设计 -->
<!-- <PreviewModal ref="previewModal" /> -->
</GiPageLayout> </GiPageLayout>
</template> </template>
@ -107,8 +97,6 @@ import {
} from '@/apis/industrial-image' } from '@/apis/industrial-image'
import DeformationTap from './components/DeformationTap.vue' import DeformationTap from './components/DeformationTap.vue'
//
// const previewModal = ref()
// //
const activeTab = ref('image') const activeTab = ref('image')

View File

@ -7,9 +7,9 @@
<template #icon><icon-arrow-left /></template> <template #icon><icon-arrow-left /></template>
</a-button> </a-button>
<h2 class="ml-2">{{ projectTitle }}</h2> <h2 class="ml-2">{{ projectTitle }}</h2>
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{ <a-tag class="ml-2" :color="getStatusColor(projectData.statusLabel ?? projectData.status)" v-if="projectData.status !== undefined && projectData.status !== null">
projectData.status {{ projectData.statusLabel ?? projectData.status }}
}}</a-tag> </a-tag>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<a-button v-permission="['project:update']" type="primary" class="mr-2" @click="editProject"> <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 { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue' import { Message, Modal } from '@arco-design/web-vue'
import { getProject, deleteProject } from '@/apis/project' import { getProjectDetail, deleteProject } from '@/apis/project'
import { addTask, addTaskGroup, listTask, updateTaskProgress } from '@/apis/project/task' import { addTask, addTaskGroup, updateTaskProgress } from '@/apis/project/task'
import dayjs from 'dayjs' import dayjs from 'dayjs'
defineOptions({ name: 'ProjectDetail' }) defineOptions({ name: 'ProjectDetail' })
@ -257,10 +257,15 @@ const projectTitle = computed(() => {
const projectInfos = computed(() => [ const projectInfos = computed(() => [
{ label: '项目编号', value: projectData.value?.projectCode }, { label: '项目编号', value: projectData.value?.projectCode },
{ label: '项目负责人', value: projectData.value?.projectManager }, { label: '项目负责人', value: projectData.value?.projectManagerName || projectData.value?.projectManager },
{ label: '参与人', value: projectData.value?.projectStaff?.join(', ') }, { label: '项目来源', value: projectData.value?.projectOrigin },
{ label: '项目周期', value: projectData.value?.projectPeriod ? `${projectData.value.projectPeriod[0]}${projectData.value.projectPeriod[1]}` : '' }, { label: '风场名称', value: projectData.value?.farmName },
{ label: '客户', value: projectData.value?.commissionUnit }, { 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 || '无' } { label: '备注', value: projectData.value?.projectIntro || '无' }
]) ])
@ -284,6 +289,15 @@ const taskDetailInfos = computed(() => {
{ label: '状态', value: currentTask.value.status }, { label: '状态', value: currentTask.value.status },
{ label: '描述', value: currentTask.value.description || '无' } { 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) => { const getStatusColor = (status: string) => {
@ -322,8 +336,14 @@ const formatDate = (date: string) => {
const fetchProjectData = async () => { const fetchProjectData = async () => {
loading.value = true loading.value = true
try { try {
const res = await getProject(projectId.value) const res = await getProjectDetail(projectId.value)
projectData.value = res.data const detail = (res as any).data || res
// statusstatusLabel
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) { } catch (error) {
console.error(error) console.error(error)
Message.error('获取项目详情失败') Message.error('获取项目详情失败')
@ -332,34 +352,39 @@ const fetchProjectData = async () => {
} }
} }
const fetchTaskData = async () => { //
try { const inferTaskStatus = (task: any): string => {
const res = await listTask({ if (task.status) return task.status
projectId: projectId.value, const now = dayjs()
page: 1, const start = task.planStartDate ? dayjs(task.planStartDate) : null
size: 100 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 '其他'
}
//
taskColumns.value.forEach(column => {
column.tasks = []
})
const tasks = res.data?.list || [] const fetchTaskData = () => {
// 使
const detail = projectData.value || {}
const tasks = (detail.tasks || []) as any[]
// //
tasks.forEach((task: any) => { taskColumns.value.forEach(column => {
const column = taskColumns.value.find(col => col.status === task.status) column.tasks = []
if (column) { })
column.tasks.push(task)
} else { //
taskColumns.value.find(col => col.status === '其他')?.tasks.push(task) tasks.forEach((task: any) => {
} const st = inferTaskStatus(task)
}) const column = taskColumns.value.find(col => col.status === st)
} catch (error) { if (column) {
console.error(error) column.tasks.push(task)
Message.error('获取任务数据失败') } else {
} taskColumns.value.find(col => col.status === '其他')?.tasks.push(task)
}
})
} }
const goBack = () => { const goBack = () => {
@ -491,8 +516,7 @@ const submitProgressUpdate = async () => {
} }
onMounted(() => { onMounted(() => {
fetchProjectData() fetchProjectData().then(() => fetchTaskData())
fetchTaskData()
}) })
</script> </script>

View File

@ -2,19 +2,21 @@
项目管理页面 项目管理页面
已完成接口对接: 已完成接口对接:
1. 项目列表查询 (listProject) - 支持分页和条件查询 1. 项目列表查询 (listProject) - 支持分页和条件查询
2. 项目新增 (addProject) 2. 项目新增 (addProject)
3. 项目修改 (updateProject) 3. 项目修改 (updateProject)
4. 项目删除 (deleteProject) 4. 项目删除 (deleteProject)
5. 项目导出 (exportProject) 5. 项目导出 (exportProject)
6. 项目导入 (importProject) 6. 项目导入 (importProject)
所有API调用都已添加错误处理和类型安全检查 所有API调用都已添加错误处理和类型安全检查
--> -->
<template> <template>
<GiPageLayout> <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']" :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> <template #top>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset"> <GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset">
</GiForm> </GiForm>
@ -71,10 +73,14 @@
</GiTable> </GiTable>
<!-- 新增/编辑项目弹窗 --> <!-- 新增/编辑项目弹窗 -->
<a-modal v-model:visible="addModalVisible" :title="modalTitle" @cancel="resetForm" <a-modal
:ok-button-props="{ loading: submitLoading }" @ok="handleSubmit" width="800px" modal-class="project-form-modal"> v-model:visible="addModalVisible" :title="modalTitle" :ok-button-props="{ loading: submitLoading }"
<a-form ref="formRef" :model="form" :rules="formRules" layout="vertical" width="800px" modal-class="project-form-modal" @cancel="resetForm" @ok="handleSubmit"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"> >
<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-divider orientation="left">基本信息</a-divider>
<a-row :gutter="16"> <a-row :gutter="16">
@ -88,10 +94,12 @@
<a-input v-model="form.farmAddress" placeholder="请输入地址" /> <a-input v-model="form.farmAddress" placeholder="请输入地址" />
</a-form-item> </a-form-item>
</a-col> </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> <template #icon><icon-location /></template>
地图选点 地图选点
</a-button></a-col> </a-button>
</a-col>
</a-row> </a-row>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
@ -108,7 +116,7 @@
<a-col :span="12"> <a-col :span="12">
<a-form-item field="inspectionUnit" label="业主"> <a-form-item field="inspectionUnit" label="业主">
<a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (form.farmName = val)" /> <a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" @input="(val) => (form.farmName = val)" />
<!--风场名称同步业主 --> <!-- 风场名称同步业主 -->
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
@ -145,10 +153,133 @@
</a-col> </a-col>
</a-row> </a-row>
<a-row :gutter="16"> <a-row :gutter="16">
<a-form-item field="projectContent" label="项目内容"> <a-col :span="12">
<a-textarea v-model="form.coverUrl" placeholder="请输入项目内容" :rows="4" /> <a-form-item field="projectOrigin" label="项目来源" :rules="[{ required: true, message: '请输入项目来源' }]">
</a-form-item> <a-input v-model="form.projectOrigin" placeholder="请输入项目来源" />
</a-form-item>
</a-col>
</a-row> </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-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item field="status" label="项目状态"> <a-form-item field="status" label="项目状态">
@ -198,7 +329,6 @@
</a-col> </a-col>
</a-row> </a-row>
<a-divider orientation="middle">地图</a-divider> <a-divider orientation="middle">地图</a-divider>
</a-form> </a-form>
</a-modal> </a-modal>
@ -221,53 +351,53 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue' import { Message, Modal } from '@arco-design/web-vue'
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 { isMobile } from '@/utils'
import has from '@/utils/has'
import http from '@/utils/http' import http from '@/utils/http'
import type { ColumnItem } from '@/components/GiForm' import type { ColumnItem } from '@/components/GiForm'
import type { TableColumnData } from '@arco-design/web-vue' import type { ProjectPageQuery } from '@/apis/project/type'
import type { ProjectResp, ProjectPageQuery } from '@/apis/project/type' import type * as T from '@/apis/project/type'
import * as T from '@/apis/project/type'
import TurbineGrid from './TurbineGrid.vue'
defineOptions({ name: 'ProjectManagement' }) defineOptions({ name: 'ProjectManagement' })
// (API) // (API)
const PROJECT_STATUS = { const PROJECT_STATUS = {
NOT_STARTED: 0, // / NOT_STARTED: 0, // /
IN_PROGRESS: 1, // IN_PROGRESS: 1, //
COMPLETED: 2, // COMPLETED: 2, //
} as const } as const
// //
const PROJECT_STATUS_MAP = { const PROJECT_STATUS_MAP = {
0: '待施工', 0: '待施工',
1: '施工中', 1: '施工中',
2: '已完成' 2: '已完成',
} as const } as const
// //
const PROJECT_STATUS_OPTIONS = [ const PROJECT_STATUS_OPTIONS = [
{ label: '待施工', value: 0 }, { label: '待施工', value: 0 },
{ label: '施工中', value: 1 }, { label: '施工中', value: 1 },
{ label: '已完成', value: 2 } { label: '已完成', value: 2 },
] ]
// //
const PROJECT_CATEGORY = { const PROJECT_CATEGORY = {
EXTERNAL_WORK: '外部工作', EXTERNAL_WORK: '外部工作',
INTERNAL_PROJECT: '内部项目', INTERNAL_PROJECT: '内部项目',
TECHNICAL_SERVICE: '技术服务' TECHNICAL_SERVICE: '技术服务',
} as const } as const
// //
const PROJECT_CATEGORY_OPTIONS = [ const PROJECT_CATEGORY_OPTIONS = [
{ label: PROJECT_CATEGORY.EXTERNAL_WORK, value: PROJECT_CATEGORY.EXTERNAL_WORK }, { label: PROJECT_CATEGORY.EXTERNAL_WORK, value: PROJECT_CATEGORY.EXTERNAL_WORK },
{ label: PROJECT_CATEGORY.INTERNAL_PROJECT, value: PROJECT_CATEGORY.INTERNAL_PROJECT }, { 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() const router = useRouter()
@ -280,12 +410,12 @@ const currentId = ref<string | null>(null)
const fileList = ref([]) const fileList = ref([])
const dataList = ref<T.ProjectResp[]>([]) const dataList = ref<T.ProjectResp[]>([])
const userLoading = ref(false) const userLoading = ref(false)
const userOptions = ref<{ label: string; value: string }[]>([]) const userOptions = ref<{ label: string, value: string }[]>([])
let searchForm = reactive<Partial<ProjectPageQuery>>({ const searchForm = reactive<Partial<ProjectPageQuery>>({
projectName: '', projectName: '',
status: undefined, status: undefined,
fieldName: '', // 使fieldNameAPI使 fieldName: '', // 使fieldNameAPI使
}) })
const queryFormColumns: ColumnItem[] = reactive([ const queryFormColumns: ColumnItem[] = reactive([
@ -320,28 +450,48 @@ const queryFormColumns: ColumnItem[] = reactive([
]) ])
const form = reactive({ const form = reactive({
projectId: '', // id projectId: '', // id
projectName: '', // projectName: '', //
projectManagerId: '', // id projectManagerId: '', // id
client: '', // client: '', //
clientContact: '', // clientContact: '', //
clientPhone: '', // clientPhone: '', //
inspectionUnit: '', // inspectionUnit: '', //
inspectionContact: '', // inspectionContact: '', //
inspectionPhone: '', // inspectionPhone: '', //
farmName: '', // farmName: '', //
farmAddress: '', // farmAddress: '', //
scale: '', // projectOrigin: '', //
turbineModel: '', // scale: '', //
status: '', // 01234 turbineModel: '', //
startDate: '', // status: '', // 01234
endDate: '', // startDate: '', //
coverUrl: '', // endDate: '', //
// coverUrl: '', // 使
constructionTeamLeaderId: '', // id constructionTeamLeaderId: '', // id
constructorIds: '', // id constructorIds: '', // id
qualityOfficerId: '', // id qualityOfficerId: '', // id
auditorId: '', // 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({ const pagination = reactive({
@ -350,7 +500,7 @@ const pagination = reactive({
total: 0, total: 0,
showTotal: true, showTotal: true,
showJumper: true, showJumper: true,
showPageSize: true showPageSize: true,
}) })
const openMapModal = (item: any) => { const openMapModal = (item: any) => {
Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`) Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`)
@ -384,70 +534,70 @@ const tableColumns = ref<TableColumnData[]>([
slotName: 'fieldInfo', slotName: 'fieldInfo',
minWidth: 180, minWidth: 180,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
slotName: 'status', slotName: 'status',
align: 'center', align: 'center',
width: 100 width: 100,
}, },
{ {
title: '委托单位', title: '委托单位',
dataIndex: 'commissionUnit', dataIndex: 'commissionUnit',
minWidth: 140, minWidth: 140,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '委托单位联系人/电话', title: '委托单位联系人/电话',
slotName: 'commissionInfo', slotName: 'commissionInfo',
minWidth: 160, minWidth: 160,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '业主', title: '业主',
dataIndex: 'inspectionUnit', dataIndex: 'inspectionUnit',
minWidth: 140, minWidth: 140,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '业主联系人/电话', title: '业主联系人/电话',
slotName: 'inspectionInfo', slotName: 'inspectionInfo',
minWidth: 160, minWidth: 160,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '项目规模', title: '项目规模',
dataIndex: 'projectScale', dataIndex: 'projectScale',
width: 100, width: 100,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '机组型号', title: '机组型号',
dataIndex: 'orgNumber', dataIndex: 'orgNumber',
width: 100, width: 100,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '项目经理/施工人员', title: '项目经理/施工人员',
slotName: 'projectManager', slotName: 'projectManager',
minWidth: 160, minWidth: 160,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '项目周期', title: '项目周期',
slotName: 'projectPeriod', slotName: 'projectPeriod',
minWidth: 180, minWidth: 180,
ellipsis: true, ellipsis: true,
tooltip: true tooltip: true,
}, },
{ {
title: '操作', title: '操作',
@ -492,7 +642,7 @@ const fetchData = async () => {
const params: ProjectPageQuery = { const params: ProjectPageQuery = {
...searchForm, ...searchForm,
page: pagination.current, page: pagination.current,
size: pagination.pageSize size: pagination.pageSize,
} }
const res = await listProject(params) const res = await listProject(params)
@ -516,9 +666,9 @@ const fetchData = async () => {
projectManager: item.projectManagerName, projectManager: item.projectManagerName,
projectScale: item.scale, projectScale: item.scale,
// //
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : [] projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : [],
}; }
return mappedItem; return mappedItem
}) })
// APItotal使 // APItotal使
@ -553,7 +703,7 @@ const reset = () => {
// //
Object.assign(searchForm, { Object.assign(searchForm, {
projectName: '', projectName: '',
fieldName: '', // 使fieldNameAPI使 fieldName: '', // 使fieldNameAPI使
status: undefined, status: undefined,
}) })
@ -576,27 +726,29 @@ const onPageSizeChange = (pageSize: number) => {
const resetForm = () => { const resetForm = () => {
// //
Object.assign(form, { Object.assign(form, {
projectId: '', // id projectId: '', // id
projectName: '', // projectName: '', //
projectManagerId: '', // id projectManagerId: '', // id
client: '', // client: '', //
clientContact: '', // clientContact: '', //
clientPhone: '', // clientPhone: '', //
inspectionUnit: '', // inspectionUnit: '', //
inspectionContact: '', // inspectionContact: '', //
inspectionPhone: '', // inspectionPhone: '', //
farmName: '', // farmName: '', //
farmAddress: '', // farmAddress: '', //
scale: '', // projectOrigin: '', //
turbineModel: '', // scale: '', //
status: 0, // 01234 turbineModel: '', //
startDate: '', // status: 0, // 01234
endDate: '', // startDate: '', //
coverUrl: '', // endDate: '', //
// coverUrl: '', //
constructionTeamLeaderId: '', // id constructionTeamLeaderId: '', // id
constructorIds: '', // id constructorIds: '', // id
qualityOfficerId: '', // id qualityOfficerId: '', // id
auditorId: '' // id auditorId: '', // id
tasks: [],
}) })
isEdit.value = false isEdit.value = false
@ -607,21 +759,49 @@ const openAddModal = () => {
resetForm() resetForm()
addModalVisible.value = true 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) => { const openEditModal = (record: T.ProjectResp) => {
isEdit.value = true isEdit.value = true
currentId.value = record.id || record.projectId || null 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) }
// tasksturbineList
resetForm()
// //
Object.keys(form).forEach(key => { Object.keys(form).forEach((key) => {
if (key in record && record[key as keyof T.ProjectResp] !== undefined) { if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
// @ts-ignore - // @ts-expect-error -
form[key] = record[key as keyof T.ProjectResp] form[key] = record[key as keyof T.ProjectResp]
} }
}) })
@ -645,6 +825,7 @@ const openEditModal = (record: T.ProjectResp) => {
// //
const formRules = { const formRules = {
projectName: [{ required: true, message: '请输入项目名称' }], projectName: [{ required: true, message: '请输入项目名称' }],
projectOrigin: [{ required: true, message: '请输入项目来源' }],
} }
// //
@ -660,15 +841,70 @@ const handleSubmit = async () => {
await formRef.value.validate() 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 = { const submitData = {
...form, auditorId: (form as any).auditorId || '',
// projectId 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, projectId: isEdit.value && currentId.value ? currentId.value : form.projectId,
// - YYYY-MM-DD projectManagerId: form.projectManagerId || '',
startDate: form.startDate ? (typeof form.startDate === 'string' ? form.startDate : new Date(form.startDate).toISOString().split('T')[0]) : '', projectName: form.projectName,
endDate: form.endDate ? (typeof form.endDate === 'string' ? form.endDate : new Date(form.endDate).toISOString().split('T')[0]) : '', projectOrigin: (form as any).projectOrigin || '',
// ID - qualityOfficerId: form.qualityOfficerId || '',
constructorIds: Array.isArray(form.constructorIds) ? form.constructorIds.join(',') : form.constructorIds 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) console.log('提交数据:', submitData)
@ -753,8 +989,8 @@ const viewDetail = (record: T.ProjectResp) => {
router.push({ router.push({
name: 'ProjectDetail', name: 'ProjectDetail',
params: { params: {
id: projectId.toString() id: projectId.toString(),
} },
}) })
} }
@ -811,7 +1047,7 @@ const exportData = async () => {
const params = { const params = {
projectName: searchForm.projectName, projectName: searchForm.projectName,
status: searchForm.status, status: searchForm.status,
fieldName: searchForm.fieldName, // 使fieldNameAPI使 fieldName: searchForm.fieldName, // 使fieldNameAPI使
} }
await exportProject(params) await exportProject(params)
@ -829,9 +1065,9 @@ const fetchUserList = async () => {
// //
const res = await http.get('/user/list') const res = await http.get('/user/list')
if (res.data && Array.isArray(res.data)) { 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 || '未命名用户', label: item.userName || item.username || item.name || item.nickName || item.account || '未命名用户',
value: item.userId || item.id || '' value: item.userId || item.id || '',
})) }))
} else { } else {
userOptions.value = [] userOptions.value = []

View File

@ -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>

View File

@ -19,6 +19,12 @@
@search="handleSearch" @search="handleSearch"
@reset="handleReset" @reset="handleReset"
/> />
<a-button type="outline" @click="handleAddEquipmentType" size="large">
<template #icon>
<IconTag />
</template>
添加设备类型
</a-button>
<a-button type="primary" @click="handleAdd" size="large"> <a-button type="primary" @click="handleAdd" size="large">
<template #icon> <template #icon>
<IconPlus /> <IconPlus />
@ -235,6 +241,14 @@
:mode="modalMode" :mode="modalMode"
@success="handleModalSuccess" @success="handleModalSuccess"
/> />
<!-- 设备类型管理弹窗 -->
<EquipmentTypeModal
v-model:visible="equipmentTypeModalVisible"
:type-data="currentEquipmentType"
:mode="equipmentTypeModalMode"
@success="handleEquipmentTypeModalSuccess"
/>
</div> </div>
</template> </template>
@ -250,9 +264,11 @@ import {
IconPlus, IconPlus,
IconRefresh, IconRefresh,
IconSearch, IconSearch,
IconTag,
} from '@arco-design/web-vue/es/icon' } from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message' import message from '@arco-design/web-vue/es/message'
import DeviceModal from './components/DeviceModal.vue' import DeviceModal from './components/DeviceModal.vue'
import EquipmentTypeModal from './components/EquipmentTypeModal.vue'
import EquipmentSearch from './components/EquipmentSearch.vue' import EquipmentSearch from './components/EquipmentSearch.vue'
import router from '@/router' import router from '@/router'
import { EquipmentAPI } from '@/apis' import { EquipmentAPI } from '@/apis'
@ -287,6 +303,11 @@ const modalVisible = ref(false)
const currentEquipment = ref<EquipmentResp | null>(null) const currentEquipment = ref<EquipmentResp | null>(null)
const modalMode = ref<'add' | 'edit' | 'view'>('add') const modalMode = ref<'add' | 'edit' | 'view'>('add')
//
const equipmentTypeModalVisible = ref(false)
const currentEquipmentType = ref<any>(null)
const equipmentTypeModalMode = ref<'add' | 'edit'>('add')
// //
const columns = [ const columns = [
{ {
@ -913,6 +934,19 @@ const handleModalSuccess = () => {
loadData() loadData()
} }
//
const handleAddEquipmentType = () => {
equipmentTypeModalMode.value = 'add'
currentEquipmentType.value = null
equipmentTypeModalVisible.value = true
}
//
const handleEquipmentTypeModalSuccess = () => {
equipmentTypeModalVisible.value = false
loadData()
}
// //
watch(tableData, (_newData) => { watch(tableData, (_newData) => {
// //

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -178,6 +178,20 @@
<span v-else class="no-data">-</span> <span v-else class="no-data">-</span>
</template> </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 }"> <template #action="{ record }">
<a-space> <a-space>
@ -196,6 +210,40 @@
> >
申请采购 申请采购
</a-button> </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 <a-tag
v-if="record.procurementStatus && record.procurementStatus !== 'NOT_STARTED'" v-if="record.procurementStatus && record.procurementStatus !== 'NOT_STARTED'"
@ -252,6 +300,32 @@
:equipment-data="currentApplicationData" :equipment-data="currentApplicationData"
@success="handleApplicationSuccess" @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> </div>
</template> </template>
@ -271,6 +345,10 @@ import message from '@arco-design/web-vue/es/message'
import ProcurementModal from './components/ProcurementModal.vue' import ProcurementModal from './components/ProcurementModal.vue'
import ProcurementSearch from './components/ProcurementSearch.vue' import ProcurementSearch from './components/ProcurementSearch.vue'
import ProcurementApplicationModal from './components/ProcurementApplicationModal.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 { equipmentProcurementApi } from '@/apis/equipment/procurement'
import { equipmentApprovalApi } from '@/apis/equipment/approval' import { equipmentApprovalApi } from '@/apis/equipment/approval'
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type' import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
@ -307,6 +385,18 @@ const modalMode = ref<'add' | 'edit' | 'view'>('add')
const applicationModalVisible = ref(false) const applicationModalVisible = ref(false)
const currentApplicationData = ref<EquipmentResp | null>(null) 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 selectedRowKeys = ref<string[]>([])
const rowSelection = reactive({ const rowSelection = reactive({
@ -418,6 +508,20 @@ const columns = [
slotName: 'createTime', slotName: 'createTime',
width: 160, width: 160,
}, },
{
title: '收货状态',
dataIndex: 'receiptStatus',
key: 'receiptStatus',
slotName: 'receiptStatus',
width: 120,
},
{
title: '支付状态',
dataIndex: 'paymentStatus',
key: 'paymentStatus',
slotName: 'paymentStatus',
width: 120,
},
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
@ -527,6 +631,50 @@ const getHealthStatusText = (status: string) => {
return textMap[status] || '未知' 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) => { const formatPrice = (price: number) => {
return price.toLocaleString('zh-CN', { return price.toLocaleString('zh-CN', {
@ -601,6 +749,8 @@ const transformBackendData = (data: any[]): EquipmentResp[] => {
inventoryBasis: item.inventoryBasis, inventoryBasis: item.inventoryBasis,
dynamicRecord: item.dynamicRecord, dynamicRecord: item.dynamicRecord,
procurementStatus: item.procurementStatus, procurementStatus: item.procurementStatus,
receiptStatus: item.receiptStatus || 'NOT_RECEIVED',
paymentStatus: item.paymentStatus || 'NOT_PAID',
})) }))
} }
@ -850,6 +1000,54 @@ const canApplyProcurement = (record: EquipmentResp) => {
return canApply 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 getApprovalStatusColor = (status: string) => {
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {

View File

@ -6,7 +6,7 @@
<p class="page-description">审批转交的任务</p> <p class="page-description">审批转交的任务</p>
</div> </div>
<!-- 状态汇总改为左边文字右边数字布局 --> <!-- 状态汇总改为左边文字右边数字布局 -->
<div class="status-summary"> <div class="status-summary">
<div <div
class="status-item pending" class="status-item pending"
@ -200,47 +200,51 @@ const handleReject = (taskId: string) => {
/* 待审批状态样式 */ /* 待审批状态样式 */
.status-item.pending { .status-item.pending {
border-top-color: #f59e0b; background-color: #f59e0b;
background-color: rgba(245, 158, 11, 0.1); /* 浅橙色背景 */ background-image: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
} }
.status-item.pending .count { .status-item.pending .count {
color: #f59e0b; color: #fff;
} }
/* 已通过状态样式 */ /* 已通过状态样式 */
.status-item.approved { .status-item.approved {
border-top-color: #10b981; background-color: #10b981;
background-color: rgba(16, 185, 129, 0.1); /* 浅绿色背景 */ background-image: linear-gradient(135deg, #10b981 0%, #059669 100%);
} }
.status-item.approved .count { .status-item.approved .count {
color: #10b981; color: #fff;
} }
/* 已拒绝状态样式 */ /* 已拒绝状态样式 */
.status-item.rejected { .status-item.rejected {
border-top-color: #ef4444; background-color: #ef4444;
background-color: rgba(239, 68, 68, 0.1); /* 浅红色背景 */ background-image: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
} }
.status-item.rejected .count { .status-item.rejected .count {
color: #ef4444; color: #fff;
} }
/* 状态文字样式 */ /* 状态文字样式 */
.status-text { .status-text {
font-size: 14px; font-size: 16px;
color: #333; font-weight: 500;
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
} }
/* 数字样式 */ /* 数字样式 */
.count { .count {
font-size: 20px; font-size: 28px;
font-weight: bold; font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
letter-spacing: 0.5px;
} }
/* 激活态样式(可选,点击后高亮) */ /* 激活态样式(可选,点击后高亮) */
.status-item.active { .status-item.active {
transform: scale(1.02); 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 { .content {

View File

@ -1,38 +1,711 @@
<template> <template>
<GiPageLayout> <GiPageLayout>
<div class="task-progress-page"> <div class="task-tracking-page">
<!-- 页面标题 --> <!-- 固定标题和表头容器 -->
<div class="page-header"> <div class="sticky-headers">
<h2>任务跟踪</h2> <!-- 页面标题 -->
<p class="page-description">跟踪监控和评估任务的完成情况</p> <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>
<!-- 分组容器按重要紧急程度分组 -->
<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> </div>
</GiPageLayout> </GiPageLayout>
</template> </template>
<script setup lang="ts"> <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> </script>
<style scoped> <style scoped>
.task-progress-page { /* 页面基础样式 */
height: 100%; .task-tracking-page {
padding: 20px; padding: 20px;
display: flex; background: #fff;
flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
} }
.page-header { .page-header {
margin-bottom: 24px; margin-bottom: 20px;
padding-bottom: 16px; }
.page-header h2 {
font-size: 20px;
font-weight: bold;
margin-bottom: 5px;
color: #333;
} }
.page-description { .page-description {
color: #666; color: #666;
margin-top: 8px;
font-size: 14px; 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> </style>

View File

@ -173,6 +173,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import TaskForm from './components/TaskForm.vue'; import TaskForm from './components/TaskForm.vue';
import AssigneeSelector from './components/AssigneeSelector.vue'; import AssigneeSelector from './components/AssigneeSelector.vue';
const taskFormRef = ref<InstanceType<typeof TaskForm> | null>(null); const taskFormRef = ref<InstanceType<typeof TaskForm> | null>(null);
@ -330,15 +331,15 @@ const resetSearch = () => {
const handleSubmit = () => { const handleSubmit = () => {
if (!taskFormRef.value?.form.taskName) { if (!taskFormRef.value?.form.taskName) {
alert('请填写任务名称'); Message.error('请填写任务名称');
return; return;
} }
if (!taskFormRef.value?.form.dueDate) { if (!taskFormRef.value?.form.dueDate) {
alert('请设置截止日期'); Message.error('请设置截止日期');
return; return;
} }
if (!assigneeRef.value?.assignees.leader) { if (!assigneeRef.value?.assignees.leader) {
alert('请选择任务负责人'); Message.error('请选择任务负责人');
return; return;
} }
const taskData = { const taskData = {
@ -361,7 +362,7 @@ const handleSubmit = () => {
priority: taskData.priority || 'medium' priority: taskData.priority || 'medium'
}); });
console.log('发布任务数据:', taskData); console.log('发布任务数据:', taskData);
alert('任务发布成功!'); Message.success('任务发布成功!');
showPublishModal.value = false; showPublishModal.value = false;
}; };
</script> </script>

View File

@ -56,7 +56,7 @@ const filteredUsers = ref<typeof users.value>([]);
const filterUsersByDepartment = () => { const filterUsersByDepartment = () => {
if (!selectedDepartment.value) { if (!selectedDepartment.value) {
filteredUsers.value = []; filteredUsers.value = [];
assignees.value.leader = ''; assignees.value.leader = -1;
return; return;
} }
@ -66,7 +66,7 @@ const filterUsersByDepartment = () => {
// //
if (assignees.value.leader && !filteredUsers.value.some(u => u.id === assignees.value.leader)) { 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({ const assignees = ref({
leader: '' leader: 0
}); });
// //

View File

@ -119,6 +119,34 @@ defineExpose({
</script> </script>
<style scoped> <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 { .error-message {
color: #ef4444; color: #ef4444;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>