29 Commits

Author SHA1 Message Date
dhr
9407ad5446 0928 2025-09-28 18:03:50 +08:00
dhr
3606ab7cf8 0928 2025-09-28 17:31:02 +08:00
Teo
11f9433ba7 合并 2025-09-28 17:29:25 +08:00
Teo
b6ec72acee Merge branch 'lx' of http://192.168.110.2:3000/taoge_xiaodi/maintenance_system 2025-09-28 17:28:16 +08:00
Teo
3fa5b39fc3 Merge branch 'dhr' of http://192.168.110.2:3000/taoge_xiaodi/maintenance_system 2025-09-28 17:27:19 +08:00
dhr
4a31c7d028 0928 2025-09-28 17:23:00 +08:00
dhr
3f07f7afe3 0926 2025-09-26 20:32:14 +08:00
086b52f88f 采购管理: 新增采购计划相关功能及组件
文件上传: 增加拖拽上传功能并优化组件逻辑
库存管理: 移除表格固定高度以改善显示效果
采购计划: 添加类型定义文件及接口文档
2025-09-26 20:05:38 +08:00
dd32d930d7 feat(物资管理): 新增备品配件和出入库单管理功能
实现备品配件管理模块,包括列表展示、搜索、新增、编辑、删除功能
完成出入库单管理功能,支持单据类型切换、搜索筛选和增删改查操作
添加数据统计图表展示出入库情况
优化表单验证和错误处理逻辑
2025-09-25 20:03:45 +08:00
dhr
6b9bfb66b1 0925 2025-09-25 20:03:08 +08:00
d626d72d43 feat: 更新物料管理模块功能
1. 新增采购计划草稿存储功能
2. 优化出入库单和备件管理界面
3. 完善表单验证和交互逻辑
4. 调整表格列对齐方式
5. 移除冗余的审批备注字段
ps:出入口页面未完成
2025-09-24 20:06:58 +08:00
tcy
33831ecad3 fix: 修复分页请求和预置点添加功能
在spjk.vue中添加isflow参数确保分页请求正确
在presetAdd.vue中使用ElLoading替代原有loading实现更好的用户体验
2025-09-24 20:04:56 +08:00
tcy
d68f537537 Merge branch 'lx' of http://xny.yj-3d.com:3000/taoge_xiaodi/maintenance_system into tcy 2025-09-24 17:53:36 +08:00
tcy
b7c716509d fix(devicePreset): 修改删除预置位接口参数及调用方式
refactor(securitySurveillance): 添加项目ID参数到监控列表请求
style(camera): 注释掉未使用的设备操作按钮代码
2025-09-24 17:52:51 +08:00
dhr
9913a7854c 0924 2025-09-24 16:37:09 +08:00
tcy
64c538775f feat(securitySurveillance): 实现首页大屏数据展示和设备状态动态更新
- 新增获取首页大屏数据的API接口
- 在安全监控页面添加数据获取逻辑并传递给子组件
- 更新设备状态组件显示实时在线/离线数据
- 优化视频监控组件播放器初始化和销毁逻辑
- 调整API接口路径和参数格式
- 移除无用代码和注释
2025-09-24 16:31:18 +08:00
bab5b8a856 Merge branch 'tcy' of http://192.168.110.2:3000/taoge_xiaodi/maintenance_system into lx 2025-09-24 10:41:30 +08:00
tcy
4163b11d3d feat(视频监控): 新增摄像头预置位管理和视频监控功能
新增摄像头预置位管理功能,包括添加、修改、删除和调用预置位
实现视频监控页面,支持扩展视图和普通视图切换
添加获取摄像头列表接口,优化视频播放器初始化逻辑
完善分页功能,根据视图类型动态调整请求数量
2025-09-23 20:37:04 +08:00
dhr
80cca114a9 0922 2025-09-23 20:36:47 +08:00
30f5941202 排班管理接口对接 2025-09-23 20:15:50 +08:00
tcy
f79eecd247 Merge branch 'lx' of http://xny.yj-3d.com:3000/taoge_xiaodi/maintenance_system into tcy 2025-09-23 17:48:57 +08:00
tcy
bbca5c8961 feat(视频监控): 实现萤石云视频播放功能并优化布局交互
- 添加ezuikit-js依赖用于视频播放
- 实现视频播放器初始化、销毁和切换逻辑
- 优化视频布局交互,支持扩展/普通视图切换
- 添加视频控制按钮和悬停效果
- 更新开发环境API地址
2025-09-23 17:38:52 +08:00
tcy
31c1732af5 feat: 优化UI组件样式和交互逻辑
- 为对话框添加背景图片并调整布局
- 修改视频监控组件的小视频切换逻辑
- 调整天气卡片样式增加内边距和背景
- 修复视频监控组件展开/收起状态判断
- 为安全监控页面添加顶部间距
2025-09-23 10:50:47 +08:00
07c5dcde11 1.新增排班时间管理页面及其对接接口
2.对接排班人员列表接口
3.修改部分样式
2025-09-22 20:47:13 +08:00
tcy
6d960a1fc7 feat(站点概览): 添加状态和告警自定义弹窗组件
refactor(样式): 重构弹窗样式并分离状态和告警样式
style: 调整页面间距和布局
2025-09-22 19:47:06 +08:00
bc158f9bd5 Merge branch 'master' of http://192.168.110.2:3000/taoge_xiaodi/maintenance_system into lx 2025-09-22 17:54:44 +08:00
tcy
c027533d4f feat(对话框样式): 重构对话框样式并添加详细信息展示
- 调整对话框布局和样式,增加顶部对齐
- 添加设备状态、运行信息等详细展示区域
- 实现不同状态的颜色区分显示
- 默认隐藏对话框,改为点击触发显示
2025-09-22 16:49:50 +08:00
bf44c0c34d Merge branch 'tcy' of http://192.168.110.2:3000/taoge_xiaodi/maintenance_system into lx 2025-09-22 16:16:30 +08:00
f0609716bc 1.完成生产管理-电量分析静态界面
2.完成综合管理-人员排班管理交互
3.修改部分逻辑和样式
2025-09-22 16:15:50 +08:00
84 changed files with 19630 additions and 4339 deletions

View File

@ -5,7 +5,7 @@ VITE_APP_TITLE = 新能源场站智慧运维平台
VITE_APP_ENV = 'development'
# 开发环境
VITE_APP_BASE_API = 'http://192.168.110.149:18899'
VITE_APP_BASE_API = 'http://192.168.110.210:18899'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/'

View File

@ -32,6 +32,7 @@
"echarts-gl": "^2.0.9",
"echarts-liquidfill": "^3.1.0",
"element-plus": "2.9.8",
"ezuikit-js": "^8.1.10",
"file-saver": "2.0.5",
"highlight.js": "11.9.0",
"image-conversion": "2.1.1",

BIN
public/assets/dialog2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB

View File

@ -0,0 +1,75 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DevicePresetVO, DevicePresetForm, DevicePresetQuery } from '@/api/camera/devicePreset/types';
/**
* 查询摄像头预置位列表
* @param query
* @returns {*}
*/
export const listDevicePreset = (query?: DevicePresetQuery): AxiosPromise<DevicePresetVO[]> => {
return request({
url: '/ops/devicePreset/list',
method: 'get',
params: query
});
};
/**
* 查询摄像头预置位详细
* @param id
*/
export const getDevicePreset = (id: string | number): AxiosPromise<DevicePresetVO> => {
return request({
url: '/ops/devicePreset/' + id,
method: 'get'
});
};
/**
* 新增摄像头预置位
* @param data
*/
export const addDevicePreset = (data: DevicePresetForm) => {
return request({
url: '/ops/devicePreset',
method: 'post',
data: data
});
};
/**
* 修改摄像头预置位
* @param data
*/
export const updateDevicePreset = (data: DevicePresetForm) => {
return request({
url: '/ops/devicePreset',
method: 'put',
data: data
});
};
/**
* 删除摄像头预置位
* @param id
*/
export const delDevicePreset = (data: any) => {
return request({
url: '/ops/devicePreset/delYzd',
method: 'delete',
data: [data]
});
};
/**
* 调用摄像头预置位
* @param data
*/
export const callDevicePreset = (data: DevicePresetForm) => {
return request({
url: '/ops/devicePreset/callYzd',
method: 'post',
data: data
});
};

View File

@ -0,0 +1,86 @@
export interface DevicePresetVO {
/**
* 主键id
*/
id: string | number;
/**
* 设备序列号
*/
deviceSerial: string;
/**
* 通道号
*/
channelNo: number;
/**
* 预置点序号
*/
presetIndex: number;
/**
* 预置点
*/
presetName: string;
}
export interface DevicePresetForm extends BaseEntity {
/**
* 主键id
*/
id?: string | number;
/**
* 设备序列号
*/
deviceSerial?: string;
/**
* 通道号
*/
channelNo?: number;
/**
* 预置点序号
*/
presetIndex?: number;
/**
* 预置点
*/
presetName?: string;
}
export interface DevicePresetQuery extends PageQuery {
/**
* 设备序列号
*/
deviceSerial?: string;
/**
* 通道号
*/
channelNo?: number;
/**
* 预置点序号
*/
presetIndex?: number;
/**
* 预置点
*/
presetName?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@ -0,0 +1,79 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { SchedulingVO } from './types';
/**
* 查询排班人员列表
* @param deptId
*/
export function getPaibanRenYuanList(deptId:string | number): AxiosPromise<any> {
return request({
url: `/system/user/list/dept/`+deptId,
method: 'get',
});
}
/**
* 查询运维-人员排班列表
*/
export function getPaibanRiLiList(query?: SchedulingVO): AxiosPromise<SchedulingVO[]> {
return request({
url: `/ops/personnel/scheduling/getRiLiList`,
method: 'get',
params: query
});
}
/**
* 运维-人员排班-查询排班列表
*/
export function getPaibanListPage(query?: SchedulingVO): AxiosPromise<SchedulingVO[]> {
return request({
url: `/ops/personnel/scheduling/list`,
method: 'get',
params: query
});
}
/**
* 运维-人员排班-安排排班
*/
export function savePaiban(data: any): AxiosPromise<any> {
return request({
url: `/ops/personnel/scheduling/all`,
method: 'post',
data: data
});
}
/**
* 运维-人员排班-修改排班
*/
export function updatePaiban(data:any): AxiosPromise<any> {
return request({
url: `/ops/personnel/scheduling`,
method: 'put',
data: data
});
}
/**
* 运维-人员排班-批量修改排班
*/
// export function updateAllPaiban(): AxiosPromise<any> {
// return request({
// url: `/ops/personnel/scheduling/all`,
// method: 'put',
// });
// }
/**
* 运维-人员排班-删除排班
*/
export function deletePaiban(ids: string): AxiosPromise<any> {
return request({
url: `/ops/personnel/scheduling/${ids}`,
method: 'delete',
});
}

View File

@ -0,0 +1,39 @@
export interface SchedulingVO {
/**
* 开始时间
*/
schedulingStartDate: string;
/**
* 结束时间
*/
schedulingEndDate: string;
/**
* 部门ID
*/
projectId?: string | number;
}
// export interface SchedulingQuery extends PageQuery {
// /**
// * 开始时间
// */
// schedulingStartDate: string;
// /**
// * 结束时间
// */
// schedulingEndDate: string;
// /**
// * 部门ID
// */
// projectId?: string | number;
// }

View File

@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { SchedulingDateVO, SchedulingDateForm, SchedulingDateQuery } from '@/api/renyuan/schedulingDate/types';
/**
* 查询运维-排班时间类型列表
* @param query
* @returns {*}
*/
export const listSchedulingDate = (query?: SchedulingDateQuery): AxiosPromise<SchedulingDateVO[]> => {
return request({
url: '/ops/personnel/schedulingDate/list',
method: 'get',
params: query
});
};
/**
* 查询运维-排班时间类型详细
* @param id
*/
export const getSchedulingDate = (id: string | number): AxiosPromise<SchedulingDateVO> => {
return request({
url: '/ops/personnel/schedulingDate/' + id,
method: 'get'
});
};
/**
* 新增运维-排班时间类型
* @param data
*/
export const addSchedulingDate = (data: SchedulingDateForm) => {
return request({
url: '/ops/personnel/schedulingDate',
method: 'post',
data: data
});
};
/**
* 修改运维-排班时间类型
* @param data
*/
export const updateSchedulingDate = (data: SchedulingDateForm) => {
return request({
url: '/ops/personnel/schedulingDate',
method: 'put',
data: data
});
};
/**
* 删除运维-排班时间类型
* @param id
*/
export const delSchedulingDate = (id: string | number | Array<string | number>) => {
return request({
url: '/ops/personnel/schedulingDate/' + id,
method: 'delete'
});
};

View File

@ -0,0 +1,86 @@
export interface SchedulingDateVO {
/**
* id
*/
id: string | number;
/**
* 排班名称
*/
schedulingName: string;
/**
* 开始时间
*/
startTime: string;
/**
* 结束时间
*/
endTime: string;
/**
* 部门ID
*/
projectId?: string | number;
}
export interface SchedulingDateForm extends BaseEntity {
/**
* id
*/
id?: string | number;
/**
* 排班名称
*/
schedulingName?: string;
/**
* 开始时间
*/
startTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 部门ID
*/
projectId?: string | number;
}
export interface SchedulingDateQuery extends PageQuery {
/**
* 排班名称
*/
schedulingName?: string;
/**
* 开始时间
*/
startTime?: string;
/**
* 结束时间
*/
endTime?: string;
/**
* 部门ID
*/
projectId?: string | number;
/**
* 日期范围参数
*/
params?: any;
}

View File

@ -0,0 +1,23 @@
import request from '@/utils/request';
// 获取萤石云Token
export function getToken() {
return request({
url: '/ops/monitoriing/getToken',
method: 'get',
})
}
// 获取摄像头列表
export function getMonitoringList(data) {
return request({
url: '/ops/monitoriing/getMonitoringList',
method: 'post',
data
})
}
// 获取首页大屏数据
export function getHomeScreenData() {
return request({
url: '/ops/monitoriing/getMonitoringDp',
method: 'get',
})
}

View File

@ -29,10 +29,11 @@ export const treeselect = (params?: any): AxiosPromise<MenuTreeOption[]> => {
};
// 根据角色ID查询菜单下拉树结构
export const roleMenuTreeselect = (roleId: string | number): AxiosPromise<RoleMenuTree> => {
export const roleMenuTreeselect = (roleId: string | number, params?: any): AxiosPromise<RoleMenuTree> => {
return request({
url: '/system/menu/roleMenuTreeselect/' + roleId,
method: 'get'
method: 'get',
params
});
};

View File

@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { BeipinBeijianVO, BeipinBeijianForm, BeipinBeijianQuery } from '@/api/wuziguanli/beijian/types';
/**
* 查询运维-物资-备品配件列表
* @param query
* @returns {*}
*/
export const listBeipinBeijian = (query?: BeipinBeijianQuery): AxiosPromise<BeipinBeijianVO[]> => {
return request({
url: '/ops/beipinBeijian/list',
method: 'get',
params: query
});
};
/**
* 查询运维-物资-备品配件详细
* @param id
*/
export const getBeipinBeijian = (id: string | number): AxiosPromise<BeipinBeijianVO> => {
return request({
url: '/ops/beipinBeijian/' + id,
method: 'get'
});
};
/**
* 新增运维-物资-备品配件
* @param data
*/
export const addBeipinBeijian = (data: BeipinBeijianForm) => {
return request({
url: '/ops/beipinBeijian',
method: 'post',
data: data
});
};
/**
* 修改运维-物资-备品配件
* @param data
*/
export const updateBeipinBeijian = (data: BeipinBeijianForm) => {
return request({
url: '/ops/beipinBeijian',
method: 'put',
data: data
});
};
/**
* 删除运维-物资-备品配件
* @param id
*/
export const delBeipinBeijian = (id: string | number | Array<string | number>) => {
return request({
url: '/ops/beipinBeijian/' + id,
method: 'delete'
});
};

View File

@ -0,0 +1,131 @@
export interface BeipinBeijianVO {
/**
* id
*/
id: string | number;
/**
* 项目id
*/
projectId: string | number;
/**
* 备件编号
*/
beijianNumber: string;
/**
* 备件名称
*/
beijianName: string;
/**
* 设备类型
*/
shebeiType: string;
/**
* 规格型号
*/
guigexinghao: string;
/**
* 库存状态(待定)
*/
kucunStatus: string;
/**
* 库存数量
*/
kucunCount: number;
}
export interface BeipinBeijianForm extends BaseEntity {
/**
* id
*/
id?: string | number;
/**
* 项目id
*/
projectId?: string | number;
/**
* 备件编号
*/
beijianNumber?: string;
/**
* 备件名称
*/
beijianName?: string;
/**
* 设备类型
*/
shebeiType?: string;
/**
* 规格型号
*/
guigexinghao?: string;
/**
* 库存状态(待定)
*/
kucunStatus?: string;
/**
* 库存数量
*/
kucunCount?: number;
}
export interface BeipinBeijianQuery extends PageQuery {
/**
* 项目id
*/
projectId?: string | number;
/**
* 备件编号
*/
beijianNumber?: string;
/**
* 备件名称
*/
beijianName?: string;
/**
* 设备类型
*/
shebeiType?: string;
/**
* 规格型号
*/
guigexinghao?: string;
/**
* 库存状态(待定)
*/
kucunStatus?: string;
/**
* 库存数量
*/
kucunCount?: number;
/**
* 日期范围参数
*/
params?: any;
}

View File

@ -0,0 +1,56 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { CaigouPlanVO, CaigouPlanForm, CaigouPlanQuery } from '@/api/wuziguanli/caigouPlan/types';
/**
* 查询运维-物资-采购计划单列表
* @param query
* @returns {*}
*/
export const listCaigouPlan = (query?: CaigouPlanQuery): AxiosPromise<CaigouPlanVO[]> => {
return request({
url: '/ops/caigouPlan/list',
method: 'get',
params: query
});
};
/**
* 查询采购商列表
* @param query
* @returns {*}
*/
export const getSupplierList = (data:any): AxiosPromise<any> => {
return request({
url: '/ops/tenderSupplierInput/getList',
method: 'get',
params: data
});
};
/**
* 新增运维-物资-采购计划单
* @param data
* @returns {*}
*/
export const addCaigouPlan = (data: CaigouPlanForm): AxiosPromise<CaigouPlanVO> => {
return request({
url: '/ops/caigouPlan',
method: 'post',
data: data
});
};
/**
* 查询运维-物资-采购计划单详情
* @param id
* @returns {*}
*/
export const caigouPlanDetail = (id: string | number): AxiosPromise<CaigouPlanVO> => {
return request({
url: `/ops/caigouPlan/`+id,
method: 'get'
});
};

View File

@ -0,0 +1,558 @@
export interface CaigouPlanVO {
/**
* id
*/
id: string | number;
/**
* 项目id
*/
projectId: string | number;
/**
* 计划名称
*/
jihuaName: string;
/**
* 计划编号
*/
jihuaBianhao: string;
/**
* 采购单位(当前登录人部门)
*/
caigouDanwei: number;
/**
* 采购单位名称
*/
caigouDanweiName: string;
/**
* 经办人
*/
jingbanren: number;
/**
* 经办人名称
*/
jingbanrenName: string;
/**
* 合同类型
*/
hetonType: string;
/**
* 采购类型
*/
caigouType: string;
/**
* 仓库地址
*/
cangkuUrl: string;
/**
* 合同名称
*/
hetonName: string;
/**
* 供应商id
*/
gonyingshangId: string | number;
/**
* 出货时间
*/
chuhuoTime: string;
/**
* 付款条件
*/
fukuantiaojian: string;
/**
* 发票开具方式
*/
fapiaoKjfs: string;
/**
* 计划状态
*/
status: string;
/**
* 审核状态
*/
shenheStatus: string;
/**
* 预计金额
*/
yujiJine: number;
/**
* 实际采购金额
*/
shijiJine: number;
/**
* 文件id
*/
fileId: string | number;
/**
* 文件地址
*/
fileUrl: string;
/**
* 文件名称
*/
fileName: string;
/**
* 采购申请计划id
*/
caigouPlanId: string | number;
/**
* 产品名称
*/
chanpinName: string;
/**
* 产品型号
*/
chanpinType: string;
/**
* 产品单价
*/
chanpinMonovalent: number;
/**
* 购买数量
*/
goumaiNumber: number;
/**
* 单位
*/
danwei: string;
/**
* 用途
*/
yontu: string;
/**
* 总价
*/
totalPrice: number;
/**
* 申请时间
*/
createTime?: string;
/**
* 出货时间
*/
chouhuoTime?: string;
/**
* 采购申请计划文件 新增
*/
opsCaigouPlanFilesBos?: Array<any>;
/**
* 采购申请计划产品 新增
*/
opsCaigouPlanChanpinBos?:Array<any>;
/**
* 采购申请计划产品 查询
*/
opsCaigouPlanChanpinVos?: Array<any>;
/**
* 采购申请计划文件 查询
*/
opsCaigouPlanFilesVos?: Array<any>;
}
export interface CaigouPlanForm extends BaseEntity {
/**
* id
*/
id?: string | number;
/**
* 项目id
*/
projectId?: string | number;
/**
* 计划名称
*/
jihuaName?: string;
/**
* 计划编号
*/
jihuaBianhao?: string;
/**
* 采购单位(当前登录人部门)
*/
caigouDanwei?: number;
/**
* 采购单位名称
*/
caigouDanweiName?: string;
/**
* 经办人
*/
jingbanren?: number;
/**
* 经办人名称
*/
jingbanrenName?: string;
/**
* 合同类型
*/
hetonType?: string;
/**
* 采购类型
*/
caigouType?: string;
/**
* 仓库地址
*/
cangkuUrl?: string;
/**
* 合同名称
*/
hetonName?: string;
/**
* 供应商id
*/
gonyingshangId?: string | number;
/**
* 出货时间
*/
chuhuoTime?: string;
/**
* 付款条件
*/
fukuantiaojian?: string;
/**
* 发票开具方式
*/
fapiaoKjfs?: string;
/**
* 计划状态
*/
status?: string;
/**
* 审核状态
*/
shenheStatus?: string;
/**
* 预计金额
*/
yujiJine?: number;
/**
* 实际采购金额
*/
shijiJine?: number;
/**
* 采购申请计划id
*/
caigouPlanId?: string | number;
/**
* 文件id
*/
fileId?: string | number;
/**
* 文件地址
*/
fileUrl?: string;
/**
* 文件名称
*/
fileName?: string;
/**
* 产品名称
*/
chanpinName?: string;
/**
* 产品型号
*/
chanpinType?: string;
/**
* 产品单价
*/
chanpinMonovalent?: number;
/**
* 购买数量
*/
goumaiNumber?: number;
/**
* 单位
*/
danwei?: string;
/**
* 用途
*/
yontu?: string;
/**
* 总价
*/
totalPrice?: number;
/**
* 采购申请计划文件 新增
*/
opsCaigouPlanFilesBos?: Array<any>;
/**
* 采购申请计划产品 新增
*/
opsCaigouPlanChanpinBos?:Array<any>;
/**
* 采购申请计划产品 查询
*/
opsCaigouPlanChanpinVos?: Array<any>;
/**
* 采购申请计划文件 查询
*/
opsCaigouPlanFilesVos?: Array<any>;
/**
* 申请时间
*/
createTime?: string;
/**
* 出货时间
*/
chouhuoTime?: string;
}
export interface CaigouPlanQuery extends PageQuery {
/**
* 项目id
*/
projectId?: string | number;
/**
* 计划名称
*/
jihuaName?: string;
/**
* 计划编号
*/
jihuaBianhao?: string;
/**
* 采购单位(当前登录人部门)
*/
caigouDanwei?: number;
/**
* 采购单位名称
*/
caigouDanweiName?: string;
/**
* 经办人
*/
jingbanren?: number;
/**
* 经办人名称
*/
jingbanrenName?: string;
/**
* 合同类型
*/
hetonType?: string;
/**
* 采购类型
*/
caigouType?: string;
/**
* 仓库地址
*/
cangkuUrl?: string;
/**
* 合同名称
*/
hetonName?: string;
/**
* 供应商id
*/
gonyingshangId?: string | number;
/**
* 出货时间
*/
chuhuoTime?: string;
/**
* 付款条件
*/
fukuantiaojian?: string;
/**
* 发票开具方式
*/
fapiaoKjfs?: string;
/**
* 计划状态
*/
status?: string;
/**
* 审核状态
*/
shenheStatus?: string;
/**
* 预计金额
*/
yujiJine?: number;
/**
* 实际采购金额
*/
shijiJine?: number;
/**
* 日期范围参数
*/
params?: any;
/**
* 采购申请计划id
*/
caigouPlanId?: string | number;
/**
* 文件id
*/
fileId?: string | number;
/**
* 文件地址
*/
fileUrl?: string;
/**
* 文件名称
*/
fileName?: string;
/**
* 产品名称
*/
chanpinName?: string;
/**
* 产品型号
*/
chanpinType?: string;
/**
* 产品单价
*/
chanpinMonovalent?: number;
/**
* 购买数量
*/
goumaiNumber?: number;
/**
* 单位
*/
danwei?: string;
/**
* 用途
*/
yontu?: string;
/**
* 总价
*/
totalPrice?: number;
/**
* 采购申请计划文件 新增
*/
opsCaigouPlanFilesBos?: Array<any>;
/**
* 采购申请计划产品 新增
*/
opsCaigouPlanChanpinBos?:Array<any>;
/**
* 采购申请计划产品 查询
*/
opsCaigouPlanChanpinVos?: Array<any>;
/**
* 采购申请计划文件 查询
*/
opsCaigouPlanFilesVos?: Array<any>;
/**
* 申请时间
*/
createTime?: string;
/**
* 出货时间
*/
chouhuoTime?: string;
}

View File

@ -0,0 +1,76 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { ChurukudanVO, ChurukudanForm, ChurukudanQuery } from '@/api/wuziguanli/churuku/types';
/**
* 查询运维-物资-出入库单管理列表
* @param query
* @returns {*}
*/
export const listChurukudan = (query?: ChurukudanQuery): AxiosPromise<ChurukudanVO[]> => {
return request({
url: '/ops/churukudan/list',
method: 'get',
params: query
});
};
/**
* 查询运维-物资-出入库单管理详细
* @param id
*/
export const getChurukudan = (id: string | number): AxiosPromise<ChurukudanVO> => {
return request({
url: '/ops/churukudan/' + id,
method: 'get'
});
};
/**
* 新增运维-物资-出入库单管理
* @param data
*/
export const addChurukudan = (data: ChurukudanForm) => {
return request({
url: '/ops/churukudan',
method: 'post',
data: data
});
};
/**
* 修改运维-物资-出入库单管理
* @param data
*/
export const updateChurukudan = (data: ChurukudanForm) => {
return request({
url: '/ops/churukudan',
method: 'put',
data: data
});
};
/**
* 删除运维-物资-出入库单管理
* @param id
*/
export const delChurukudan = (id: string | number | Array<string | number>) => {
return request({
url: '/ops/churukudan/' + id,
method: 'delete'
});
};
/**
* 运维-物资-出入库单柱状图
* @param query
* @returns {*}
*/
export const getChuRuKuCountBar = (data:any): AxiosPromise<any> => {
return request({
url: '/ops/churukudan/getChuRuKuCount',
method: 'get',
params: data
});
};

View File

@ -0,0 +1,154 @@
export interface ChurukudanVO {
/**
* id
*/
id: string | number;
/**
* 项目id
*/
projectId: string | number;
/**
* 单据编号
*/
danjvNumber: string;
/**
* 设备类型
*/
shebeiType: string;
/**
* 经手人id
*/
jingshourenId: string | number;
/**
* 经手人
*/
jingshourenName: string;
/**
* 联系电话
*/
contactNumber: string;
/**
* 总数量
*/
zonNumber: number;
/**
* 审核状态
*/
shenheStatus: string;
/**
* 单据状态1、出库单2入库单
*/
danjvType: string;
}
export interface ChurukudanForm extends BaseEntity {
/**
* id
*/
id?: string | number;
/**
* 项目id
*/
projectId: string | number;
/**
* 单据编号
*/
danjvNumber?: string;
/**
* 设备类型
*/
shebeiType?: string;
/**
* 经手人id
*/
jingshourenId?: string | number;
/**
* 经手人
*/
jingshourenName?: string;
/**
* 联系电话
*/
contactNumber?: string;
/**
* 总数量
*/
zonNumber?: number;
/**
* 审核状态
*/
shenheStatus?: string;
/**
* 单据状态1、出库单2入库单
*/
danjvType?: string;
/**
* 审核状态
*/
auditStatus?: string;
}
export interface ChurukudanQuery extends PageQuery {
/**
* 项目id
*/
projectId?: string | number;
/**
* 单据编号
*/
danjvNumber?: string;
/**
* 设备类型
*/
shebeiType?: string;
/**
* 审核状态
*/
shenheStatus?: string;
/**
* 单据状态1、出库单2入库单
*/
danjvType?: string;
/**
* 审核状态
*/
auditStatus?: string;
/**
* 开始日期
*/
startDate?: string;
/**
* 结束日期
*/
endDate?: string;
/**
* 日期范围参数
*/
params?: any;
}

View File

@ -47,3 +47,11 @@ export const uploadbaoxiu = (data) => {
data: data
});
};
export const baoxiuRecord = (data) => {
return request({
url: '/ops/report/record',
method: 'get',
params: data
});
};

View File

@ -0,0 +1,57 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
//查询列表
export const gongdanlist = (query) => {
return request({
url: '/ops/order/list',
method: 'get',
params: query
});
};
//新增待办事项
export const addgongdan = (data) => {
return request({
url: '/ops/order',
method: 'post',
data: data
});
};
//修改待办事项
export const updategongdan = (data) => {
return request({
url: '/ops/order',
method: 'put',
data: data
});
};
//删除待办事项
export function delgongdan(ids) {
return request({
url: `/ops/order/${ids}`, // 拼接ids作为路径参数
method: 'delete'
});
}
export const gongdanDetail = (id) => {
return request({
url: `/ops/order/${id}`,
method: 'get'
});
};
export const uploadgongdan = (data) => {
return request({
url: '/resource/oss/upload',
method: 'post',
data: data
});
};
export const gongdanRecord = (data) => {
return request({
url: '/ops/order/record',
method: 'get',
params: data
});
};

View File

@ -0,0 +1,57 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
//查询列表
export const jiedianlist = (query) => {
return request({
url: '/ops/node/list',
method: 'get',
params: query
});
};
//新增待办事项
export const addjiedian = (data) => {
return request({
url: '/ops/node',
method: 'post',
data: data
});
};
//修改待办事项
export const updatejiedian = (data) => {
return request({
url: '/ops/node',
method: 'put',
data: data
});
};
//删除待办事项
export function deljiedian(ids) {
return request({
url: `/ops/node/${ids}`, // 拼接ids作为路径参数
method: 'delete'
});
}
export const jiedianDetail = (id) => {
return request({
url: `/ops/node/${id}`,
method: 'get'
});
};
export const uploadjiedian = (data) => {
return request({
url: '/resource/oss/upload',
method: 'post',
data: data
});
};
export const jiedianRecord = (data) => {
return request({
url: '/ops/node/record',
method: 'get',
params: data
});
};

View File

@ -0,0 +1,57 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
//查询列表
export const qiangxiulist = (query) => {
return request({
url: '/ops/repair/list',
method: 'get',
params: query
});
};
//新增待办事项
export const addqiangxiu = (data) => {
return request({
url: '/ops/repair',
method: 'post',
data: data
});
};
//修改待办事项
export const updateqiangxiu = (data) => {
return request({
url: '/ops/repair',
method: 'put',
data: data
});
};
//删除待办事项
export function delqiangxiu(ids) {
return request({
url: `/ops/repair/${ids}`, // 拼接ids作为路径参数
method: 'delete'
});
}
export const qiangxiuDetail = (id) => {
return request({
url: `/ops/repair/${id}`,
method: 'get'
});
};
export const uploadqiangxiu = (data) => {
return request({
url: '/resource/oss/upload',
method: 'post',
data: data
});
};
export const qiangxiuRecord = (data) => {
return request({
url: '/ops/repair/record',
method: 'get',
params: data
});
};

View File

@ -39,3 +39,11 @@ export const syrenwuDetail = (id) => {
method: 'get'
});
};
export const syrenwujilu = (data) => {
return request({
url: '/ops/testTask/record',
method: 'get',
params: data
});
};

View File

@ -34,7 +34,7 @@ export const delxunjian = (ids) => {
//查询人员
export const xunjianUserlist = (query) => {
return request({
url: '/ops/constructionUser/list',
url: '/system/user/list',
method: 'get',
params: query
});

16
src/assets/styles/1.html Normal file
View File

@ -0,0 +1,16 @@
<div class="card">
<div id="content">
</div>
</div>
<script type="text/javascript">
// 定义每个状态对应的图片URL
const titleList = ['运行正常', '运行异常', '未运行']
let titleHtml = ""
titleList.forEach((title, index) => {
titleHtml += `我是标题${title}<br>`
})
document.getElementById('content').innerHTML = titleHtml
</script>

View File

@ -1,16 +1,230 @@
.no-header-dialog {
height: auto;
}
#custom-dialog {
padding: 0;
top: 0;
.el-dialog__header {
display: none;
// display: none;
border: none;
padding: 0;
margin: 0;
}
.el-dialog__body {
padding: 0 !important;
// height: auto !important;
max-height: none !important;
}
.alert-content {
padding: 80px;
.status-alert-content {
background: linear-gradient(180deg, rgba(0, 119, 255, 0.23) 0%, rgba(255, 255, 255, 0) 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 20px;
padding-right: 50px;
.info {
display: flex;
flex-direction: column;
gap: 10px;
.title {
color: rgba(0, 30, 59, 1);
font-size: 20px;
font-weight: bold;
}
.name {
color: rgba(0, 30, 59, 1);
font-weight: bold;
}
.icon {
display: flex;
align-items: center;
font-size: 12px;
.last-update {
// font-size: ;
color: rgba(113, 128, 150, 1);
margin-left: 15px;
}
svg {
width: 15px;
height: 15px;
}
.text {
font-size: 12px;
margin-left: 10px;
}
}
}
.img {
width: 240px;
height: 240px;
img {
width: 100%;
height: 100%;
display: block;
}
}
.info-box {
font-size: 12px;
display: flex;
gap: 40px;
margin-left: 30px;
.item {
display: flex;
flex-direction: column;
gap: 20px;
}
.title {
color: rgba(113, 128, 150, 1);
margin-bottom: 10px;
}
.text {
font-weight: bold;
color: rgba(0, 30, 59, 1);
}
}
}
.status-alert-content .success {
color: rgba(0, 184, 122, 1) !important;
}
.status-alert-content .orange {
color: rgba(255, 153, 0, 1) !important;
}
.status-alert-content .red {
color: rgba(227, 39, 39, 1) !important;
}
.back {
background-image: url("/assets/dialog2.png");
background-size: 455px;
background-repeat: no-repeat;
background-position: 780px -65px;
}
.alarm-alert-content {
background: linear-gradient(180deg, rgba(255, 87, 51, 0.23) 0%, rgba(255, 219, 219, 0) 100%);
padding-left: 50px;
padding-right: 50px;
padding-bottom: 50px;
.top {
display: flex;
gap: 50px;
align-items: center;
padding: 50px 0;
padding-bottom: 20px;
.info {
display: flex;
flex-direction: column;
gap: 15px;
.title {
color: rgba(227, 39, 39, 1);
font-size: 28px;
font-weight: bold;
}
.alarm-id {
color: rgba(0, 30, 59, 1);
font-size: 18px;
font-weight: bold;
}
.status-box {
display: flex;
gap: 20px;
.status {
font-weight: bold;
}
.last-update {
color: rgba(113, 128, 150, 1);
}
}
}
.info-box {
.list {
display: flex;
gap: 90px;
.item {
display: flex;
flex-direction: column;
gap: 30px;
.title {
color: rgba(113, 128, 150, 1);
margin-bottom: 10px;
}
.text {
color: rgba(0, 30, 59, 1);
font-weight: bold;
}
}
}
}
}
.progress-box {
.title {
color: rgba(0, 30, 59, 1);
font-weight: bold;
font-size: 20px;
margin-bottom: 24px;
}
}
.notice-box {
display: flex;
justify-content: space-between;
}
.item {
display: flex;
flex-direction: column;
gap: 10px;
color: rgba(113, 128, 150, 1);
.time {
font-size: 12px;
}
}
.title.active {
color: rgba(247, 89, 10, 1);
font-weight: bold;
}
}
.alarm-alert-content .red {
color: rgba(227, 39, 39, 1) !important;
}
}

View File

@ -3,6 +3,7 @@
<el-upload
ref="fileUploadRef"
multiple
:drag="isDrag"
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
@ -17,7 +18,13 @@
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button>
<el-button type="primary" v-if="!isDrag">选取文件</el-button>
<div v-else>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽文件到此处 <em>点击上传</em>
</div>
</div>
</el-upload>
<!-- 上传提示 -->
<div v-if="showTip && !disabled" class="el-upload__tip">
@ -63,11 +70,13 @@ const props = defineProps({
// 是否显示提示
isShowTip: propTypes.bool.def(true),
// 禁用组件(仅查看文件)
disabled: propTypes.bool.def(false)
disabled: propTypes.bool.def(false),
// 是否开启拖拽上传
isDrag: propTypes.bool.def(false)
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits(['update:modelValue', 'update:fileList']);
const number = ref(0);
const uploadList = ref<any[]>([]);
@ -80,6 +89,7 @@ const showTip = computed(() => props.isShowTip && (props.fileType || props.fileS
const fileUploadRef = ref<ElUploadInstance>();
// 监听 fileType 变化,更新 fileAccept
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
@ -164,6 +174,7 @@ const handleUploadSuccess = (res: any, file: UploadFile) => {
url: res.data.url,
ossId: res.data.ossId
});
uploadedSuccessfully();
} else {
number.value--;
@ -189,6 +200,7 @@ const uploadedSuccessfully = () => {
uploadList.value = [];
number.value = 0;
emit('update:modelValue', listToString(fileList.value));
emit('update:fileList', fileList.value);
proxy?.$modal.closeLoading();
}
};

View File

@ -1,5 +1,5 @@
<template>
<el-row>
<el-row v-if="titleStatus">
<el-col>
<div style="color: rgba(0, 30, 59, 1);font-family: 'Alibaba-PuHuiTi-Bold';margin: 10px 0 0 0;"
:style="{ fontSize: fontLevelMap[props.fontLevel] }">
@ -11,10 +11,10 @@
{{ props.subtitle }}
</p>
</el-col>
</el-row>
</template>
<script setup>
const titleStatus = ref(false)
const props = defineProps({
title: String,
subtitle: String,

View File

@ -0,0 +1,80 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import $cache from '@/plugins/cache';
// 草稿数据类型
export interface ProcurementDraft {
id: string;
draftNumber: string;
planName: string;
saveTime: string;
content: any;
}
// 保存草稿到本地存储
const saveDraftsToStorage = (drafts: ProcurementDraft[]) => {
$cache.local.setJSON('procurementDrafts', drafts);
};
// 从本地存储获取草稿
const getDraftsFromStorage = (): ProcurementDraft[] => {
const stored = $cache.local.getJSON('procurementDrafts');
return stored && Array.isArray(stored) ? stored : [];
};
export const useProcurementDraftStore = defineStore('procurementDraft', () => {
const draftList = ref<ProcurementDraft[]>(getDraftsFromStorage());
// 保存草稿
const saveDraft = (planName: string, content: any): ProcurementDraft => {
const today = new Date();
const dateStr = today.getFullYear() + '-' +
String(today.getMonth() + 1).padStart(2, '0') + '-' +
String(today.getDate()).padStart(2, '0');
const randomNum = Math.floor(100 + Math.random() * 900);
const draftNumber = `DRAFT-${dateStr}-${randomNum}`;
const newDraft: ProcurementDraft = {
id: `draft_${Date.now()}_${randomNum}`,
draftNumber,
planName,
saveTime: new Date().toLocaleString(),
content: JSON.parse(JSON.stringify(content)) // 深拷贝内容
};
// 添加到草稿列表并保存到本地存储
draftList.value.unshift(newDraft);
saveDraftsToStorage(draftList.value);
return newDraft;
};
// 获取草稿列表
const getDraftList = (): ProcurementDraft[] => {
return draftList.value;
};
// 获取单个草稿
const getDraft = (draftId: string): ProcurementDraft | undefined => {
return draftList.value.find(draft => draft.id === draftId);
};
// 删除草稿
const deleteDraft = (draftId: string): boolean => {
const index = draftList.value.findIndex(draft => draft.id === draftId);
if (index !== -1) {
draftList.value.splice(index, 1);
saveDraftsToStorage(draftList.value);
return true;
}
return false;
};
return {
draftList,
saveDraft,
getDraftList,
getDraft,
deleteDraft
};
});

70
src/utils/getDate.ts Normal file
View File

@ -0,0 +1,70 @@
// 获取指定月份的日期信息
export interface DateInfo {
date: number;
weekDay: string;
fullDate: string;
}
/**
* 获取当前月份的日期信息
* @returns 包含当月所有日期信息的数组
*/
export const getCurrentMonthDates = (): DateInfo[] => {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth(); // 0-11
// 获取当月第一天
const firstDay = new Date(year, month, 1);
// 获取当月最后一天
const lastDay = new Date(year, month + 1, 0);
// 当月总天数
const daysInMonth = lastDay.getDate();
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const dates: DateInfo[] = [];
// 生成当月所有日期信息
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const weekDayIndex = date.getDay(); // 0-60表示星期日
dates.push({
date: i,
weekDay: weekdays[weekDayIndex],
fullDate: `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`
});
}
return dates;
};
/**
* 获取指定月份的日期信息
* @param year 年份
* @param month 月份0-11
* @returns 包含指定月份所有日期信息的数组
*/
export const getMonthDates = (year: number, month: number): DateInfo[] => {
// 获取当月第一天
const firstDay = new Date(year, month, 1);
// 获取当月最后一天
const lastDay = new Date(year, month + 1, 0);
// 当月总天数
const daysInMonth = lastDay.getDate();
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const dates: DateInfo[] = [];
// 生成当月所有日期信息
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const weekDayIndex = date.getDay(); // 0-60表示星期日
dates.push({
date: i,
weekDay: weekdays[weekDayIndex],
fullDate: `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`
});
}
return dates;
};

View File

@ -316,3 +316,58 @@ export const removeClass = (ele: HTMLElement, cls: string) => {
export const isExternal = (path: string) => {
return /^(https?:|http?:|mailto:|tel:)/.test(path);
};
/**
* 获取步骤状态对应的样式类
* @param {string|number} status - 步骤状态码
* @returns {string} 样式类名
*/
export const getStatusClass = (status: string | number): string => {
// 处理可能的数字输入
const statusStr = status?.toString() || '';
const statusClassMap: Record<string, string> = {
'1': 'status-pending',
'2': 'status-executing',
'3': 'status-completed',
'4': 'status-delayed',
'5': 'status-failed'
};
return statusClassMap[statusStr] || 'status-unknown';
};
/**
* 格式化日期时间(用于步骤条)
* @param {string} dateTime - 日期时间字符串
* @returns {string} 格式化后的日期时间
*/
export const formatDateTime = (dateTime: string): string => {
if (!dateTime) return '-';
try {
const date = new Date(dateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (error) {
return dateTime;
}
};
/**
* 获取步骤状态文本
* @param {string|number} status - 步骤状态码
* @returns {string} 状态文本
*/
export const getStepStatusText = (status: string | number): string => {
const statusStr = status?.toString() || '';
const statusMap: Record<string, string> = {
'1': '待执行',
'2': '执行中',
'3': '已完成',
'4': '已延期',
'5': '失败'
};
return statusMap[statusStr] || '未知状态';
};

View File

@ -23,12 +23,18 @@ export const globalHeaders = () => {
};
};
// 设置默认请求头
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
axios.defaults.headers['Accept'] = 'application/json, text/plain, */*';
axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000
timeout: 50000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Accept': 'application/json, text/plain, */*'
}
});
// 请求拦截器

View File

@ -0,0 +1,309 @@
<template>
<div class="system-busPresettingBit-add">
<el-dialog v-model="isShowDialog" width="1250px" :close-on-click-modal="false" :destroy-on-close="true"
@close="closeDialog">
<template #header>
<div
v-drag="['.system-busPresettingBit-add .el-dialog', '.system-busPresettingBit-add .el-dialog__header']">
{{ title }}:添加摄像头预置位
</div>
</template>
<div class="info_list">
<div class="video_box">
<div class="video-container" id="video-container" style="width: 870px; height: 600px"></div>
</div>
<div>
<el-button type="primary" style="margin: 0 20px 10px" @click="addPre">
<el-icon>
<Plus />
</el-icon>
添加预置点
</el-button>
<el-table v-loading="loading" :data="tableData.data" border>
<el-table-column label="序号" align="center" type="index" width="55" />
<el-table-column label="名称" align="center" prop="presetName" width="120px"
show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.presetName" placeholder="请输入内容"
@change="handleEdit(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="135px">
<template #default="scope">
<el-button type="primary" link @click="handleDebug(scope.row)">
<el-icon>
<View />
</el-icon>调用
</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">
<el-icon>
<DeleteFilled />
</el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination style="padding: 5px 16px" v-show="tableData.total > 0" :total="tableData.total"
v-model:page="tableData.param.pageNum" v-model:limit="tableData.param.pageSize"
@pagination="busPresettingBitList" :layout="layout" />
</div>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, onBeforeUnmount, getCurrentInstance, nextTick } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import { listDevicePreset, addDevicePreset, updateDevicePreset, delDevicePreset, callDevicePreset } from '@/api/devicePreset';
import { getToken } from '@/api/securitySurveillance/index.js';
import EZUIKit from 'ezuikit-js';
import { ca } from 'element-plus/es/locale/index.mjs';
const emit = defineEmits(['update']);
const { proxy } = getCurrentInstance() as any;
const formRef = ref<HTMLElement | null>(null);
const menuRef = ref();
const loading = ref(false);
const isShowDialog = ref(false);
const layout = ref('total, prev, pager, next');
const title = ref('');
const updateRow = ref<any>(null);
const src = ref(null);
const flvPlayer = ref<any>(null);
const formData = ref({
deviceSerial: undefined,
channelNo: '1',
presetName: undefined
});
const tableData = ref({
data: [],
total: 0,
loading: false,
param: {
pageNum: 1,
pageSize: 15,
deviceSerial: ''
}
});
// 打开弹窗
function openDialog(row: any) {
resetForm();
updateRow.value = row;
title.value = row.deviceName;
formData.value.deviceSerial = row.deviceSerial;
tableData.value.param.deviceSerial = row.deviceSerial;
isShowDialog.value = true;
busPresettingBitList();
nextTick(() => {
videoPlay(row);
});
}
// 添加预置点
function addPre() {
ElMessageBox.prompt('请输入预置点名称', '添加预置点', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputErrorMessage: '请输入预置点名称'
})
.then(({ value }) => {
// 加载动画
const loading = ElLoading.service({
lock: true,
text: '添加中',
background: 'rgba(0, 0, 0, 0.7)',
})
formData.value.presetName = value;
addDevicePreset(formData.value)
.then(() => {
ElMessage.success('添加成功');
busPresettingBitList();
})
.finally(() => {
// loading.value = false;
loading.close();
});
})
.catch(() => { });
}
// 视频播放
function videoPlay(obj: any) {
console.log('objobjobj', obj);
getToken().then((res: any) => {
if (res.msg == "ok" && obj.deviceSerial) {
flvPlayer.value = new EZUIKit.EZUIKitPlayer({
audio: '0',
id: 'video-container',
accessToken: res.data,
url: `ezopen://open.ys7.com/${obj.deviceSerial}/1.hd.live`,
template: 'pcLive',
width: 870,
height: 600,
plugin: ['talk'],
handleError: function (err: any) {
console.log(err);
if (err?.data?.ret === 20020) {
// 20020 是并发连接限制的错误码
ElMessage.error('当前观看人数已达上限,请稍后再试');
}
}
});
}
});
}
// 关闭弹窗
function closeDialog() {
if (flvPlayer.value) {
flvPlayer.value.destroy().then((data: any) => {
console.log('promise 获取 数据', data);
});
flvPlayer.value = null;
}
isShowDialog.value = false;
}
// 获取列表
function busPresettingBitList() {
loading.value = true;
listDevicePreset(tableData.value.param).then((res: any) => {
tableData.value.data = res.rows ?? [];
tableData.value.total = res.total;
loading.value = false;
});
}
// 取消
function onCancel() {
closeDialog();
}
// 删除
function handleDelete(row: any) {
let msg = '你确定要删除所选数据?';
let id: number[] = [];
if (row) {
msg = '此操作将永久删除数据,是否继续?';
id = [row.id];
}
if (id.length === 0) {
ElMessage.error('请选择要删除的数据。');
return;
}
ElMessageBox.confirm(msg, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
const obj = {
deviceSerial: row.deviceSerial,
ids: id
};
delDevicePreset({
id: row.id,
deviceSerial: row.deviceSerial,
channelNo: "1",
presetIndex: row.presetIndex
}).then((res: any) => {
if (res.code === 200) {
ElMessage.success('删除成功');
busPresettingBitList();
}
});
})
.catch(() => { });
}
// 调用
function handleDebug(row: any) {
callDevicePreset([{
deviceSerial: row.deviceSerial,
presetIndex: row.presetIndex,
channelNo: "1",
id: row.id
}]).then((res: any) => {
if (res.code === 200) {
ElMessage.success('调用成功');
}
});
}
// 修改
function handleEdit(row: any) {
const param = {
id: row.id,
deviceSerial: row.deviceSerial,
presetName: row.presetName
};
updateDevicePreset(param)
.then(() => {
ElMessage.success('修改成功');
busPresettingBitList();
})
.finally(() => {
loading.value = false;
});
}
// 重置表单
function resetForm() {
formData.value = {
deviceSerial: undefined,
channelNo: '1',
presetName: undefined
};
}
onBeforeUnmount(() => {
if (flvPlayer.value) {
flvPlayer.value.destroy().then((data: any) => {
console.log('promise 获取 数据', data);
});
flvPlayer.value = null;
}
});
// ✅ 关键:暴露方法给父组件调用
defineExpose({
openDialog,
closeDialog
});
</script>
<style scoped lang="scss">
.system-busPresettingBit-add {
.info_list {
width: 100%;
display: flex;
height: 100%;
.video_box {
width: 75%;
height: 600px;
margin-right: 10px;
.iframe {
border: none;
outline: none;
}
.video_air {
width: 100%;
height: 100%;
object-fit: fill;
}
}
}
}
</style>

374
src/views/camera/index.vue Normal file
View File

@ -0,0 +1,374 @@
<template>
<div class="system-ys7Devices-container">
<el-card shadow="hover">
<div class="system-ys7Devices-search mb8">
<el-form :model="tableData.param" ref="queryRef" :inline="true" label-width="100px">
<el-row>
<el-col :span="8" class="colBlock">
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="tableData.param.deviceName" placeholder="请输入设备名称" clearable
@keyup.enter.native="ys7DevicesList" />
</el-form-item>
</el-col>
<el-col :span="8" class="colBlock">
<el-form-item label="设备类型" prop="deviceType">
<el-input v-model="tableData.param.deviceType" placeholder="请输入设备类型" clearable
@keyup.enter.native="ys7DevicesList" />
</el-form-item>
</el-col>
<el-col :span="8" :class="!showAll ? 'colBlock' : 'colNone'">
<el-form-item>
<el-button type="primary" @click="ys7DevicesList"><el-icon>
<Search />
</el-icon>搜索</el-button>
<el-button @click="resetQuery(queryRef)"><el-icon>
<Refresh />
</el-icon>重置</el-button>
<el-button type="primary" link @click="toggleSearch">
{{ word }}
<el-icon v-show="showAll">
<ArrowUp />
</el-icon>
<el-icon v-show="!showAll">
<ArrowDown />
</el-icon>
</el-button>
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item label="设备序列号" prop="deviceSerial">
<el-input v-model="tableData.param.deviceSerial" placeholder="请输入设备串号" clearable
@keyup.enter.native="ys7DevicesList" />
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item label="状态" prop="status">
<el-select v-model="tableData.param.status" placeholder="请选择设备状态" clearable>
<el-option label="在线" :value="1" />
<el-option label="离线" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item label="设备版本" prop="deviceVersion">
<el-input v-model="tableData.param.deviceVersion" placeholder="请输入设备版本" clearable
@keyup.enter.native="ys7DevicesList" />
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item label="所属项目" prop="projectId">
<el-select v-model="tableData.param.projectId" placeholder="请选择所属项目" clearable
filterable>
<el-option v-for="item in projectList" class="device_row" :key="item.id"
:label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item>
<el-button type="primary" @click="ys7DevicesList"><el-icon>
<Search />
</el-icon>搜索</el-button>
<el-button @click="resetQuery(queryRef)"><el-icon>
<Refresh />
</el-icon>重置</el-button>
<el-button type="primary" link @click="toggleSearch">
{{ word }}
<el-icon v-show="showAll">
<ArrowUp />
</el-icon>
<el-icon v-show="!showAll">
<ArrowDown />
</el-icon>
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
<el-button type="primary" @click="handleAdd" v-auth="'api/v1/system/ys7Devices/add'"
><el-icon><Plus /></el-icon>新增</el-button
>
</el-col> -->
<!-- <el-col :span="1.5">
<el-button type="success" :disabled="single" @click="handleUpdate(null)"
v-auth="'api/v1/system/ys7Devices/edit'"><el-icon>
<Edit />
</el-icon>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" :disabled="multiple" @click="handleDelete(null)"
v-auth="'api/v1/system/ys7Devices/delete'"><el-icon>
<Delete />
</el-icon>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" :disabled="multiple" @click="onLinkProject(null)"
v-auth="'api/v1/system/ys7Devices/add'"><el-icon>
<Link />
</el-icon>设备分配</el-button>
</el-col>-->
</el-row>
</div>
<el-table v-loading="loading" :data="tableData.data" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="设备序列号" align="center" prop="deviceSerial" min-width="100px" />
<el-table-column label="设备名称" align="center" prop="deviceName" min-width="100px" />
<el-table-column label="设备类型" align="center" prop="deviceType" min-width="100px" />
<el-table-column label="状态" align="center" prop="status" min-width="100px">
<template #default="scope">
<el-tag type="success" v-if="scope.row.status === 1">在线</el-tag>
<el-tag type="danger" v-if="scope.row.status === 0">离线</el-tag>
</template>
</el-table-column>
<!-- <el-table-column label="视频加密" align="center" prop="videoEncrypted" min-width="100px">
<template #default="scope">
<el-switch v-model="scope.row.videoEncrypted" class="ml-2" :active-value="1" :inactive-value="0"
:loading="scope.row.enctyptLoading" @change="encryptChange(scope.row)" inline-prompt
active-text="开启" inactive-text="关闭"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" />
</template>
</el-table-column> -->
<!-- <el-table-column label="" align="center" prop="defence" min-width="100px" /> -->
<el-table-column label="设备版本" align="center" prop="deviceVersion" min-width="100px" />
<!-- <el-table-column label="所属项目" align="center" prop="projectId" min-width="100px">
<template #default="scope">
{{ scope.row.projectName ? scope.row.projectName : '未分配' }}
</template>
</el-table-column> -->
<!-- <el-table-column label="备注" align="center" prop="remark" min-width="100px" /> -->
<!-- <el-table-column label="创建时间" align="center" prop="deviceCreateTime" min-width="100px">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.deviceCreateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column> -->
<el-table-column label="操作" align="center" class-name="small-padding" min-width="160px" fixed="right">
<template #default="scope">
<!-- <el-button type="primary" link @click="handleUpdate(scope.row)"
v-auth="'api/v1/system/ys7Devices/edit'"><el-icon>
<EditPen />
</el-icon>修改</el-button>
<el-button type="primary" link @click="handleDelete(scope.row)"
v-auth="'api/v1/system/ys7Devices/delete'"><el-icon>
<DeleteFilled />
</el-icon>删除</el-button>
<el-button type="primary" link @click="onLinkProject(scope.row)"
v-auth="'api/v1/system/ys7Devices/delete'"><el-icon>
<Link />
</el-icon>设备分配</el-button> -->
<el-button type="primary" link @click="addPreset(scope.row)"><el-icon>
<Plus />
</el-icon>添加预置位</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="tableData.total > 0" :total="tableData.total" v-model:page="tableData.param.pageNum"
v-model:limit="tableData.param.pageSize" @pagination="ys7DevicesList" />
</el-card>
<presetAdd ref="presetAddRef"></presetAdd>
</div>
</template>
<script setup lang="ts">
import { ElMessageBox, ElMessage, FormInstance } from 'element-plus';
import { useUserStoreHook } from '@/store/modules/user';
import { getMonitoringList } from '@/api/securitySurveillance/index.js';
import presetAdd from './components/presetAdd.vue';
// proxy 获取
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// ref 定义
const loading = ref(false);
const queryRef = ref<FormInstance>();
const addRef = ref();
const editRef = ref();
const detailRef = ref();
const bindProRef = ref();
const presetAddRef = ref();
// 展开/收起搜索项
const showAll = ref(false);
const word = computed(() => (showAll.value ? '收起搜索' : '展开搜索'));
// 多选控制
const single = ref(true);
const multiple = ref(true);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const projects = computed(() => userStore.projects);
// 状态管理
const state = reactive<any>({
ids: [],
serials: [],
tableData: {
data: [],
total: 0,
loading: false,
param: {
pageNum: 1,
pageSize: 10,
id: undefined,
createdAt: undefined,
deviceSerial: undefined,
deviceName: undefined,
deviceType: undefined,
status: undefined,
defence: undefined,
deviceVersion: undefined,
projectId: currentProject.value?.id,
dateRange: [],
isFront: false
}
},
projectList: projects.value
});
// 初始化
const initTableData = () => {
ys7DevicesList();
// sysProjectList();
};
// 搜索重置
const resetQuery = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
ys7DevicesList();
};
// 获取设备列表
const ys7DevicesList = () => {
loading.value = true;
getMonitoringList({
pageStart: state.tableData.param.pageNum,
pageSize: state.tableData.param.pageSize,
isflow: false
}).then((res: any) => {
let list = res.data.object ?? [];
state.tableData.data = list.map((item) => {
item.enctyptLoading = false;
return item;
});
state.tableData.total = Number(res.data.sum);
console.log(state.tableData);
loading.value = false;
});
};
// 展开/收起搜索项
const toggleSearch = () => {
showAll.value = !showAll.value;
};
// 多选事件
const handleSelectionChange = (selection: any[]) => {
state.ids = selection.map((item) => item.id);
state.serials = selection.map((item) => item.deviceSerial);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
// 新增
// const handleAdd = () => {
// addRef.value.openDialog();
// };
// // 编辑
// const handleUpdate = (row?: Ys7DeviceVO) => {
// if (!row) {
// row = state.tableData.data.find((item) => item.id === state.ids[0])!;
// }
// editRef.value.openDialog(toRaw(row));
// };
// 删除
// const handleDelete = (row?: any) => {
// let msg = row ? `此操作将永久删除数据,是否继续?` : '你确定要删除所选数据?';
// let id = row ? [row.id] : state.ids;
// if (id.length === 0) {
// ElMessage.error('请选择要删除的数据。');
// return;
// }
// ElMessageBox.confirm(msg, '提示', {
// confirmButtonText: '确认',
// cancelButtonText: '取消',
// type: 'warning'
// })
// .then(() => {
// delYs7Device(id).then(() => {
// ElMessage.success('删除成功');
// ys7DevicesList();
// });
// })
// .catch(() => { });
// };
// 绑定项目
// const onLinkProject = (row?: Ys7DeviceVO) => {
// let serials = row ? [row.deviceSerial] : state.ids;
// if (serials.length === 0) {
// ElMessage.error('请选择要绑定项目的设备');
// return;
// }
// let info = { serials, row };
// bindProRef.value.openDialog(toRaw(info));
// };
// 添加预置位
const addPreset = (row: any) => {
presetAddRef.value.openDialog(row);
};
// 开关加密
// const encryptChange = (row: any) => {
// row.enctyptLoading = true;
// // const action = row.videoEncrypted === 0 ? 1 : 0;
// console.log(row.videoEncrypted);
// toggleEncrypt({ videoEncrypted: row.videoEncrypted, id: row.id })
// .then(() => {
// proxy?.$modal.msgSuccess(row.videoEncrypted === 0 ? '关闭成功' : '开启成功');
// })
// .finally(() => {
// row.enctyptLoading = false;
// ys7DevicesList();
// });
// };
//监听项目id刷新数据
// const listeningProject = watch(
// () => currentProject.value?.id,
// (nid, oid) => {
// tableData.value.param.projectId = nid;
// initTableData();
// }
// );
// 页面加载
onMounted(() => {
initTableData();
});
// onUnmounted(() => {
// listeningProject();
// });
// 暴露变量
const { tableData, projectList } = toRefs(state);
</script>
<style lang="scss" scoped>
.colBlock {
display: block;
}
.colNone {
display: none;
}
</style>

View File

@ -242,7 +242,7 @@ onMounted(() => {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
height: 500px;
height: 435px;
width: 100%;
padding: 10px;
box-sizing: border-box;
@ -288,7 +288,7 @@ onMounted(() => {
@media (max-width: 768px) {
.chart-container {
height: 450px;
height: 435px;
}
}

View File

@ -0,0 +1,290 @@
<template>
<!-- 人员排班弹窗 -->
<el-dialog :model-value="manageAttendDialogVisible" @update:model-value="handleDialogVisibleChange" title="管理考勤"
width="500">
<!-- 添加表单引用和校验规则 -->
<el-form ref="attendFormRef" :model="attendForm" :rules="formRules" label-width="100px">
<el-form-item label="选择日期" prop="schedulingDate">
<el-date-picker v-model="attendForm.schedulingDate" type="date" placeholder="选择日期" style="width: 100%;"
:disabled-date="(time) => time.getTime() < Date.now() - 8.64e7" :date-format="'yyyy-MM-dd'"
value-format="YYYY-MM-DD" />
</el-form-item>
<!-- 排班类型为空提示 -->
<div v-if="shiftTypes.length === 0" class="empty-tip">
<el-alert title="暂无排班类型请先添加排班类型" type="warning" show-icon :closable="false" />
</div>
<!-- 排班人员为空提示 -->
<div v-if="props.personnelList.length === 0" class="empty-tip">
<el-alert title="暂无排班人员请先添加排班人员" type="warning" show-icon :closable="false" />
</div>
<!-- 动态排班表单 -->
<!-- 动态排班表单 -->
<div v-for="(item, index) in attendForm.userTypeBos" :key="index" class="dynamic-shift-item">
<el-form-item :label="index === 0 ? '排班设置' : ''" :required="index === 0">
<div class="shift-form-row">
<!-- 排班类型选择 -->
<el-select v-model="item.schedulingType" placeholder="请选择排班类型" style="width: 40%; margin-right: 10px;"
filterable :validate-event="false">
<!-- 使用完整的shiftTypes列表以确保已选项目也能正确显示label -->
<el-option v-for="option in shiftTypes" :key="option.value" :label="option.label" :value="option.value"
:disabled="attendForm.userTypeBos.some((bosItem) => bosItem.schedulingType === option.value && bosItem !== item)"></el-option>
</el-select>
<!-- 人员选择 -->
<el-select v-model="item.opsUserId" placeholder="请选择人员" style="width: 50%; margin-right: 10px;" multiple
filterable :validate-event="false">
<el-option v-for="person in props.personnelList" :key="person.value" :label="person.label"
:value="person.value"></el-option>
</el-select>
<!-- 删除按钮 (仅在不是第一个项目时显示) -->
<el-button v-if="index > 0" type="danger" icon="CircleCloseFilled" circle
@click="removeShiftItem(index)"></el-button>
</div>
</el-form-item>
</div>
<!-- 添加排班类型按钮 -->
<el-form-item>
<el-button type="primary" icon="CirclePlusFilled" @click="addShiftItem"
:disabled="attendForm.userTypeBos.length >= shiftTypes.length">
添加排班类型
</el-button>
<div v-if="attendForm.userTypeBos.length > 0" class="form-tip">
提示:已添加 {{ attendForm.userTypeBos.length }}/{{ shiftTypes.length }} 种排班类型
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm"
:disabled="shiftTypes.length === 0 || props.personnelList.length === 0">
确认
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
// 定义组件的props
const props = defineProps({
manageAttendDialogVisible: {
type: Boolean,
default: false
},
// 排班人员列表数据
personnelList: {
type: Array,
default: () => []
},
// 排班类型列表
typeList: {
type: Array,
default: () => []
}
});
// 定义组件的emits
const emit = defineEmits<{
'update:manageAttendDialogVisible': [value: boolean];
'confirm': [formData: any];
}>();
// 排班类型列表从传入的typeList派生
const shiftTypes = ref((props.typeList || []).map(item => ({
label: (item as { schedulingName: string }).schedulingName,
value: (item as { id: any }).id
})));
// 导入表单相关类型和消息组件
import type { FormInstance, FormRules } from 'element-plus';
import { ElMessage } from 'element-plus';
// 表单引用
const attendFormRef = ref<FormInstance>();
// 考勤表单数据
const attendForm = ref({
schedulingDate: '',
userTypeBos: [
{ schedulingType: '', opsUserId: [] }
]
});
// 表单校验规则
const formRules = ref<FormRules>({
schedulingDate: [
{ required: true, message: '请选择日期', trigger: 'change' }
]
});
// 监听typeList变化更新shiftTypes
watch(() => props.typeList, (newTypeList) => {
shiftTypes.value = (newTypeList || []).map(item => ({
label: (item as { schedulingName: string }).schedulingName,
value: (item as { id: any }).id
}));
// 当没有排班类型时,清空排班项;有排班类型但没有排班项时,添加一个空排班项
if (shiftTypes.value.length === 0) {
attendForm.value.userTypeBos = [];
} else if (attendForm.value.userTypeBos.length === 0) {
attendForm.value.userTypeBos = [{ schedulingType: '', opsUserId: [] }];
}
}, { deep: true });
// 添加新的排班类型项
const addShiftItem = () => {
// 检查是否还有可用的排班类型
if (attendForm.value.userTypeBos.length < shiftTypes.value.length) {
attendForm.value.userTypeBos.push({ schedulingType: '', opsUserId: [] });
// 现在不再需要为新添加的项添加监听器
}
};
// 删除排班类型项
const removeShiftItem = (index: number) => {
if (attendForm.value.userTypeBos.length > 1) {
attendForm.value.userTypeBos.splice(index, 1);
// 不再需要更新可用选项
}
};
// 处理取消
const handleCancel = () => {
emit('update:manageAttendDialogVisible', false);
resetForm();
};
// 处理弹窗可见性变化
const handleDialogVisibleChange = (newVisible: boolean) => {
emit('update:manageAttendDialogVisible', newVisible);
if (!newVisible) {
resetForm();
}
};
// 处理确认
const handleConfirm = async () => {
if (!attendFormRef.value) return;
try {
// 验证日期
await attendFormRef.value.validateField('schedulingDate');
// 验证每个排班项
let isValid = true;
const validationPromises: Promise<void>[] = [];
attendForm.value.userTypeBos.forEach((item, index) => {
// 验证排班类型
if (!item.schedulingType) {
isValid = false;
ElMessage.error(`${index + 1}行排班类型不能为空`);
return;
}
// 验证人员选择
if (!item.opsUserId || item.opsUserId.length === 0) {
isValid = false;
ElMessage.error(`${index + 1}行请至少选择一名人员`);
return;
}
});
if (isValid) {
// 提交表单数据给父组件
emit('confirm', attendForm.value);
emit('update:manageAttendDialogVisible', false);
resetForm();
}
} catch (error) {
// 日期验证失败
console.error('表单验证失败', error);
}
};
// 重置表单
const resetForm = () => {
attendForm.value = {
schedulingDate: '',
// 只有当有排班类型时才初始化一个空的排班项
userTypeBos: shiftTypes.value.length > 0 ? [{ schedulingType: '', opsUserId: [] }] : []
};
};
// 监听弹窗显示状态变化,在显示时重置表单
watch(() => props.manageAttendDialogVisible, (newVisible) => {
if (newVisible) {
resetForm();
}
});
</script>
<style scoped>
/* 动态排班表单样式 */
.dynamic-shift-item {
margin-bottom: 15px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.shift-form-row {
display: flex;
align-items: center;
width: 100%;
}
.form-tip {
color: #909399;
font-size: 12px;
margin-top: 8px;
margin-left: 10px;
}
/* 空数据提示样式 */
.empty-tip {
margin-bottom: 15px;
}
.empty-tip .el-alert {
margin-bottom: 10px;
}
/* 确认按钮禁用状态提示 */
.dialog-footer .el-button.is-disabled {
cursor: not-allowed;
}
/* 响应式调整 */
@media screen and (max-width: 768px) {
.shift-form-row {
flex-direction: column;
align-items: stretch;
}
.shift-form-row .el-select {
width: 100% !important;
margin-right: 0 !important;
margin-bottom: 8px;
}
.shift-form-row .el-button {
align-self: flex-start;
margin-top: 8px;
}
}
</style>

View File

@ -6,10 +6,11 @@
max-height="600"
stripe
border
v-loading="loading"
>
<!-- 固定列 -->
<el-table-column fixed prop="name" label="姓名" width="120" align="center" />
<el-table-column fixed="left" prop="position" label="岗位" width="120" align="center" />
<el-table-column fixed="left" prop="postName" label="岗位" width="120" align="center" />
<el-table-column fixed="left" prop="weeklyHours" label="周总计/小时" width="120" align="center" />
<!-- 日期列 - 纵向显示号数和星期几 -->
@ -26,97 +27,251 @@
<div class="week-day">{{ dateInfo.weekDay }}</div>
</div>
</template>
<template #default="scope">
<div
class="schedule-cell"
:class="getShiftClass(scope.row[`day${index + 1}`])"
@click="handleCellClick(scope.row, {...dateInfo, index}, scope)"
>
{{ formatShiftText(scope.row[`day${index + 1}`]) }}
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<!-- <div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div> -->
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, watch } from 'vue';
import { getCurrentMonthDates, getMonthDates } from '@/utils/getDate';
import { ElMessage } from 'element-plus';
// 员工列表
const employees = [
{ name: '张三', position: '水泥工', weeklyHours: 142 },
{ name: '李四', position: '电工', weeklyHours: 138 },
{ name: '王五', position: '木工', weeklyHours: 145 },
{ name: '赵六', position: '钢筋工', weeklyHours: 140 },
{ name: '钱七', position: '油漆工', weeklyHours: 135 },
{ name: '孙八', position: '瓦工', weeklyHours: 143 },
{ name: '周九', position: '钳工', weeklyHours: 137 },
{ name: '吴十', position: '管道工', weeklyHours: 139 },
{ name: '郑十一', position: '焊工', weeklyHours: 141 },
{ name: '王十二', position: '起重工', weeklyHours: 136 }
];
// 定义排班类型接口
interface UserTypePair {
schedulingDate: string;
schedulingType: string;
schedulingTypeName: string;
// 可能还有其他字段
[key: string]: any;
}
// 排班类型
const shifts = ['早班', '中班', '晚班', '休息'];
// 定义员工排班信息接口
interface ScheduleItem {
opsUserId: number;
opsUserName: string;
durationCount: number;
postName: string;
userTypePairs: UserTypePair[];
// 可能还有其他字段
[key: string]: any;
}
// 定义日期信息接口
interface DateInfo {
date: number;
weekDay: string;
fullDate: string;
year: number;
month: number;
}
// 定义表格行数据接口
interface TableRowData {
opsUserId: number;
name: string;
postName: string;
weeklyHours: number;
[key: string]: any; // 动态添加day1, day2等字段
}
// 定义props接收排班数据
const props = defineProps<{
scheduleList: ScheduleItem[];
loading?: boolean;
// 可选:指定要显示的月份
targetMonth?: { year: number; month: number };
}>();
const emit = defineEmits<{
'edit-schedule': [rowData: TableRowData, columnData: DateInfo & { index: number }, cellEvent: any];
'page-change': [currentPage: number, pageSize: number];
}>();
// 排班类型与样式的映射关系
const shiftConfig = {
'早班': { color: '#67c23a', className: 'morning-shift' },
'中班': { color: '#e6a23c', className: 'afternoon-shift' },
'晚班': { color: '#409eff', className: 'evening-shift' },
'休息': { color: '#909399', className: 'rest-day' },
};
// 获取当前月的日期信息
const currentMonthDates = ref<any[]>([]);
const currentMonthDates = ref<(DateInfo & { year: number; month: number })[]>([]);
// 计算当前月份并生成日期信息
const getCurrentMonthDates = () => {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth(); // 0-11
// 分页相关状态
const currentPage = ref(1);
const pageSize = ref(10);
const total = computed(() => props.scheduleList ? props.scheduleList.length : 0);
// 获取当月第一天
const firstDay = new Date(year, month, 1);
// 获取当月最后一天
const lastDay = new Date(year, month + 1, 0);
// 当月总天数
const daysInMonth = lastDay.getDate();
// 格式化排班文本,支持多排班情况
const formatShiftText = (shiftData: any): string => {
if (!shiftData) return '休息';
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const dates = [];
// 生成当月所有日期信息
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const weekDayIndex = date.getDay(); // 0-60表示星期日
dates.push({
date: i,
weekDay: weekdays[weekDayIndex],
fullDate: `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`
});
// 如果是字符串,直接返回
if (typeof shiftData === 'string') {
return shiftData;
}
return dates;
// 如果是数组,返回第一个排班类型
if (Array.isArray(shiftData)) {
return shiftData.length > 0 ? shiftData[0].schedulingTypeName || '休息' : '休息';
}
// 如果是对象,返回排班类型名称
if (typeof shiftData === 'object' && shiftData.schedulingTypeName) {
return shiftData.schedulingTypeName;
}
return '休息';
};
// 获取排班对应的样式类名
const getShiftClass = (shiftData: any): string => {
const shiftText = formatShiftText(shiftData);
return shiftConfig[shiftText as keyof typeof shiftConfig]?.className || 'unknown-shift';
};
// 生成排班数据
const scheduleData = computed(() => {
return Array.from({ length: 20 }, (_, index) => {
// 循环使用员工数据
const employee = employees[index % employees.length];
const scheduleData = computed((): TableRowData[] => {
const startIndex = (currentPage.value - 1) * pageSize.value;
const endIndex = startIndex + pageSize.value;
// 为每行生成不同的排班组合
const rowData = {
name: employee.name,
position: employee.position,
weeklyHours: employee.weeklyHours
// 确保 props.scheduleList 存在
const scheduleList = props.scheduleList || [];
// 如果没有数据且loading为false返回空数组显示空状态
if (scheduleList.length === 0 && !props.loading) {
return [];
}
// 处理排班数据
return scheduleList.map((item: ScheduleItem) => {
const rowData: TableRowData = {
opsUserId: item.opsUserId,
name: item.opsUserName || `未知员工${item.opsUserId}`,
postName: item.postName || '未知岗位',
weeklyHours: item.durationCount || 0
};
// 为当月每一天生成排班数据
currentMonthDates.value.forEach((_, dayIndex) => {
// 使用不同的种子生成略有变化的排班模式
const seed = (index * 3 + dayIndex + 1) % shifts.length;
rowData[`day${dayIndex + 1}`] = shifts[seed];
currentMonthDates.value.forEach((dateInfo, dayIndex) => {
// 格式化日期为 YYYY-MM-DD
const dateKey = `${dateInfo.year}-${String(dateInfo.month).padStart(2, '0')}-${String(dateInfo.date).padStart(2, '0')}`;
// 从userTypePairs中查找对应日期的所有排班信息
let daySchedule = null;
if (item.userTypePairs && Array.isArray(item.userTypePairs)) {
// 查找对应日期的所有排班信息
const dateSchedules = item.userTypePairs.filter(pair => pair.schedulingDate === dateKey);
// 如果有多个排班,也返回,方便后续扩展显示多个排班
daySchedule = dateSchedules.length > 0 ? dateSchedules : null;
}
// 如果找到排班信息,存储原始数据;如果没有,设置为'休息'
rowData[`day${dayIndex + 1}`] = daySchedule || '休息';
});
return rowData;
});
}).slice(startIndex, endIndex);
});
// 组件挂载时获取当前月数据
// 更新日期列表
const updateDates = () => {
if (props.targetMonth) {
// 使用指定的月份
const dates = getMonthDates(props.targetMonth.year, props.targetMonth.month - 1); // getMonthDates的month参数是0-11
currentMonthDates.value = dates.map(date => ({
...date,
year: props.targetMonth!.year,
month: props.targetMonth!.month
}));
} else {
// 使用当前月份
const today = new Date();
const dates = getCurrentMonthDates();
currentMonthDates.value = dates.map(date => ({
...date,
year: today.getFullYear(),
month: today.getMonth() + 1
}));
}
};
// 分页大小变化处理
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; // 重置为第一页
emit('page-change', currentPage.value, pageSize.value);
};
// 当前页码变化处理
const handleCurrentChange = (current: number) => {
currentPage.value = current;
emit('page-change', currentPage.value, pageSize.value);
};
// 处理单元格点击事件
const handleCellClick = (rowData: TableRowData, columnData: DateInfo & { index: number }, cellEvent: any) => {
// 获取当前单元格的排班数据
const cellData = rowData[`day${columnData.index + 1}`];
const shiftText = formatShiftText(cellData);
// 如果是休息状态,显示提示信息,不触发编辑事件
if (shiftText === '休息') {
ElMessage.warning('请前往管理考勤增加排班');
return;
}
// 非休息状态,触发编辑事件
emit('edit-schedule', rowData, columnData, cellEvent);
};
// 组件挂载时初始化
onMounted(() => {
currentMonthDates.value = getCurrentMonthDates();
updateDates();
});
// 监听目标月份变化,更新日期列表
watch(() => props.targetMonth, () => {
updateDates();
}, { deep: true });
// 监听排班数据变化,重置页码
watch(() => props.scheduleList, () => {
currentPage.value = 1;
}, { deep: true });
</script>
<style scoped>
.schedule-table-container {
overflow-x: auto;
padding: 16px;
background: #fff;
border-radius: 4px;
}
/* 优化滚动条样式 */
@ -176,4 +331,61 @@ onMounted(() => {
font-size: 12px;
color: #666;
}
/* 排班单元格样式 */
.schedule-cell {
padding: 8px 0;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
border-radius: 4px;
}
.schedule-cell:hover {
background-color: #f5f7fa;
transform: scale(1.05);
}
/* 排班类型样式 */
.morning-shift {
color: #67c23a; /* 早班 - 绿色 */
font-weight: 500;
}
.afternoon-shift {
color: #e6a23c; /* 中班 - 橙色 */
font-weight: 500;
}
.evening-shift {
color: #409eff; /* 晚班 - 蓝色 */
font-weight: 500;
}
.rest-day {
color: #909399; /* 休息 - 灰色 */
}
.unknown-shift {
color: #333; /* 未知类型 - 默认黑色 */
}
/* 分页容器样式 */
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: flex-end;
align-items: center;
}
/* 分页组件样式优化 */
:deep(.el-pagination) {
font-size: 14px;
}
/* 加载状态样式优化 */
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.8);
}
</style>

View File

@ -1,4 +1,5 @@
<template>
<!-- 考勤管理 -->
<div class="model">
<!-- 标题栏 -->
<el-row :gutter="24">
@ -45,8 +46,14 @@
<div class="analysis-content">
<attendTrend :attendData="attendData"></attendTrend>
<el-card>
<TitleComponent title="人员排班" :fontLevel="2" />
<renyuanpaiban></renyuanpaiban>
<div style="display: flex; justify-content: space-between; align-items: center;">
<TitleComponent title="人员排班" :fontLevel="2" />
<el-button type="primary" @click="manageAttendDialogVisible = true">
管理考勤
</el-button>
</div>
<renyuanpaiban @edit-schedule="handleEditSchedule" :schedule-list="scheduleList">
</renyuanpaiban>
</el-card>
</div>
</el-col>
@ -62,17 +69,311 @@
</div>
</el-col>
</el-row>
<!-- 人员排班弹窗组件 -->
<renyuanguanliDialog v-model:manageAttendDialogVisible="manageAttendDialogVisible"
@confirm="handleAttendConfirm" :personnel-list="paibanRenYuanList" :type-list="scheduleTypes" />
<!-- 编辑排班弹窗 -->
<el-dialog v-model="editScheduleDialogVisible" title="修改排班" width="400">
<el-form :model="editScheduleForm" label-width="100px">
<el-form-item label="员工姓名">
<el-input v-model="editScheduleForm.name" disabled />
</el-form-item>
<el-form-item label="排班日期">
<el-input v-model="editScheduleForm.date" disabled />
</el-form-item>
<el-form-item label="当前排班">
<el-input v-model="editScheduleForm.currentShift" disabled />
</el-form-item>
<el-form-item label="修改为">
<el-select v-model="editScheduleForm.newShift" placeholder="请选择排班类型" style="width: 100%;">
<el-option v-for="option in editscheduleTypes" :key="option.id" :label="option.schedulingName"
:value="option.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editScheduleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmEditSchedule">
确认修改
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import infoBox from '@/views/integratedManage/attendManage/components/infoBox.vue'
import attendTrend from '@/views/integratedManage/attendManage/components/attendTrend.vue'
import todayAttend from '@/views/integratedManage/attendManage/components/leftBox/todayAttend.vue'
import approval from '@/views/integratedManage/attendManage/components/leftBox/approval.vue'
import calendar from '@/views/integratedManage/attendManage/components/leftBox/calendar.vue'
import todayAttend from '@/views/integratedManage/attendManage/components/rightBox/todayAttend.vue'
import approval from '@/views/integratedManage/attendManage/components/rightBox/approval.vue'
import calendar from '@/views/integratedManage/attendManage/components/rightBox/calendar.vue'
import totalView from '@/views/integratedManage/attendManage/components/totalView.vue'
import renyuanpaiban from '@/views/integratedManage/attendManage/components/renyuanpaiban.vue'
import { ref } from 'vue';
import renyuanguanliDialog from '@/views/integratedManage/attendManage/components/renyuanguanliDialog.vue'
import { getPaibanRenYuanList, getPaibanRiLiList, savePaiban, updatePaiban, deletePaiban } from '@/api/renyuan/paiban';
import { SchedulingVO } from '@/api/renyuan/paiban/types';
import { listSchedulingDate } from '@/api/renyuan/schedulingDate';
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { getCurrentMonthDates } from '@/utils/getDate';
const currentMonthDates = getCurrentMonthDates();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 导入用户store
import { useUserStore } from '@/store/modules/user';
// 初始化用户store
const userStore = useUserStore();
// 排班人员列表
const paibanRenYuanList = ref([]);
// 排班人员数据
const scheduleList = ref<SchedulingVO[]>([]);
// 排班类型
const scheduleTypes = ref([]);
// 修改弹出框的类型下拉
const editscheduleTypes = ref([]);
// 编辑排班弹窗
const editScheduleDialogVisible = ref(false);
// 人员排班弹窗
const manageAttendDialogVisible = ref(false);
// 获取排班人员列表
const fetchPaibanRenYuanList = async (deptId?: string) => {
try {
// 如果没有提供deptId默认使用当前登录用户的部门ID
const targetDeptId = deptId || userStore.deptId;
if (!targetDeptId) {
console.warn('未提供部门ID无法获取排班人员列表');
return;
}
const response = await getPaibanRenYuanList(targetDeptId);
// console.log('获取排班人员:', response);
paibanRenYuanList.value = response.data?.map((user: any) => ({
label: user.nickName,
value: user.userId,
deptId: user.deptId,
})) || [];
} catch (error) {
console.error('获取排班人员列表失败:', error);
}
};
// 获取排班数据
const getscheduleData = async (query?: SchedulingVO) => {
try {
if (userStore.selectedProject && userStore.selectedProject.id) {
const res = await getPaibanRiLiList(query);
if (res.code === 200) {
scheduleList.value = res.data || [];
} else {
proxy?.$modal.msgError(res.msg || '获取排班数据失败');
}
}
} catch (error) {
proxy?.$modal.msgError('获取排班数据失败');
}
}
// 获取排班类型
const getTypeList = async () => {
try {
const res = await listSchedulingDate({ projectId: userStore.selectedProject?.id, pageNum: 1, pageSize: 10 });
if (res.code === 200) {
scheduleTypes.value = res.rows || [];
// 在scheduleTypes基础上新增休息字段
editscheduleTypes.value = [
...(res.rows || []),
{ id: 'rest', schedulingName: '休息' }
];
} else {
proxy?.$modal.msgError(res.msg || '获取排班类型失败');
// 如果获取失败,至少保留休息选项
editscheduleTypes.value = [{ id: 'rest', schedulingName: '休息' }];
}
} catch (error) {
console.error('获取排班类型出错:', error);
proxy?.$modal.msgError('获取排班类型失败');
// 异常情况下也保留休息选项
editscheduleTypes.value = [{ id: 'rest', schedulingTypeName: '休息' }];
}
}
// 安排人员排班
const arrangePaiban = async (formData: any) => {
try {
// 添加projectId到表单数据中
const dataWithProjectId = {
...formData,
projectId: userStore.selectedProject?.id
};
const res = await savePaiban(dataWithProjectId);
if (res.code === 200) {
proxy?.$modal.msgSuccess('排班成功');
// 刷新排班数据
refreshScheduleData(userStore.selectedProject.id);
// 关闭弹窗
manageAttendDialogVisible.value = false;
} else {
proxy?.$modal.msgError(res.msg || '排班失败');
}
} catch (error) {
proxy?.$modal.msgError('排班失败');
}
}
// 修改排班
const updateSchedule = async (formData: any) => {
try {
const res = await updatePaiban(formData);
if (res.code === 200) {
proxy?.$modal.msgSuccess('修改排班成功');
// 刷新排班数据
refreshScheduleData(userStore.selectedProject.id);
// 关闭弹窗
editScheduleDialogVisible.value = false;
} else {
proxy?.$modal.msgError(res.msg || '修改排班失败');
}
} catch (error) {
proxy?.$modal.msgError('修改排班失败');
}
}
// 删除排班
const deleteSchedule = async (ID: any) => {
try {
const res = await deletePaiban(ID);
if (res.code === 200) {
proxy?.$modal.msgSuccess('删除排班成功');
// 刷新排班数据
refreshScheduleData(userStore.selectedProject.id);
// 关闭弹窗
editScheduleDialogVisible.value = false;
} else {
proxy?.$modal.msgError(res.msg || '删除排班失败');
}
} catch (error) {
proxy?.$modal.msgError('删除排班失败');
}
}
// 处理考勤管理确认
const handleAttendConfirm = (formData: any) => {
// console.log('考勤表单数据:', formData);
// 这里可以添加表单提交逻辑
arrangePaiban(formData);
};
// 编辑排班表单数据
const editScheduleForm = ref({
opsUserId: '', // 新增opsUserId字段
name: '',
date: '',
currentShift: '',
newShift: '',
id: '' // 新增scheduledId字段用于存储排班的id
});
// 格式化排班文本,用于显示当前排班
const formatShiftDisplay = (shiftData: any): string => {
if (!shiftData) return '休息';
// 如果是字符串,直接返回
if (typeof shiftData === 'string') {
return shiftData;
}
// 如果是数组,返回第一个排班类型的名称
if (Array.isArray(shiftData) && shiftData.length > 0) {
if (typeof shiftData[0] === 'object' && shiftData[0].schedulingTypeName) {
return shiftData[0].schedulingTypeName;
}
return shiftData[0].toString();
}
// 如果是对象,返回排班类型名称
if (typeof shiftData === 'object' && shiftData.schedulingTypeName) {
return shiftData.schedulingTypeName;
}
// 如果是对象但没有schedulingTypeName属性尝试返回其字符串表示
if (typeof shiftData === 'object') {
return JSON.stringify(shiftData);
}
return '休息';
};
// 处理编辑排班
const handleEditSchedule = (rowData: any, columnData: any) => {
// 设置表单数据
editScheduleForm.value = {
opsUserId: rowData.opsUserId || '', // 从opsUserId字段获取用户ID
name: rowData.name,
date: `${columnData.fullDate}`,
currentShift: '',
newShift: '',
id: ''
};
// 查找当前排班
Object.keys(rowData).forEach(key => {
if (key.startsWith('day')) {
const dayIndex = parseInt(key.replace('day', '')) - 1;
if (dayIndex === columnData.index) {
const formattedShift = formatShiftDisplay(rowData[key]);
editScheduleForm.value.currentShift = formattedShift;
editScheduleForm.value.newShift = formattedShift;
// 如果不是休息状态则获取id并赋值到表单中
if (formattedShift !== '休息' && rowData[key]) {
// 处理rowData[key]为数组的情况
if (Array.isArray(rowData[key]) && rowData[key].length > 0 && rowData[key][0].id) {
editScheduleForm.value.id = rowData[key][0].id;
}
// 同时处理rowData[key]为对象的情况作为兼容
else if (typeof rowData[key] === 'object' && rowData[key].id) {
editScheduleForm.value.id = rowData[key].id;
}
}
}
}
});
// 显示弹窗
editScheduleDialogVisible.value = true;
};
// 确认修改排班
const handleConfirmEditSchedule = () => {
// 按照要求的格式准备数据
const submitData = {
projectId: userStore.selectedProject?.id,
opsUserId: editScheduleForm.value.opsUserId,
schedulingType: editScheduleForm.value.newShift,
schedulingDate: editScheduleForm.value.date,
id: editScheduleForm.value.id
};
console.log('提交的排班数据格式:', submitData);
if (submitData.schedulingType == 'rest') {
deleteSchedule(submitData.id);
} else {
updateSchedule(submitData);
}
};
// 出勤数据 - 用于attendTrend组件
const attendData = ref(
@ -92,46 +393,46 @@ const attendData = ref(
// Mock数据 - 更新为循环生成所需的数据结构
const totalData = ref({
attendance: {
value: 248,
change: '+8.2%',
isPositive: true,
chartData: [150, 230, 224, 218, 135, 300, 220],
color: '#FF7D00',
title: '总出勤人数',
compareText: '较昨日同期',
chartType: 'bar'
},
rest: {
value: 8,
change: '+8.2%',
isPositive: true,
chartData: [10, 12, 15, 8, 7, 9, 10],
color: '#00C48C',
title: '调休',
compareText: '较上月同期',
chartType: 'line'
},
leave: {
value: 24,
change: '-10%',
isPositive: false,
chartData: [30, 25, 28, 22, 20, 26, 24],
color: '#FF5252',
title: '本月请假',
compareText: '较昨日同期',
chartType: 'line'
},
rate: {
value: '96.8%',
change: '+10%',
isPositive: true,
chartData: [90, 92, 94, 95, 97, 98, 96.8],
color: '#029CD4',
title: '平均出勤率',
compareText: '较昨日同期',
chartType: 'line'
}
attendance: {
value: 248,
change: '+8.2%',
isPositive: true,
chartData: [150, 230, 224, 218, 135, 300, 220],
color: '#FF7D00',
title: '总出勤人数',
compareText: '较昨日同期',
chartType: 'bar'
},
rest: {
value: 8,
change: '+8.2%',
isPositive: true,
chartData: [10, 12, 15, 8, 7, 9, 10],
color: '#00C48C',
title: '调休',
compareText: '较上月同期',
chartType: 'line'
},
leave: {
value: 24,
change: '-10%',
isPositive: false,
chartData: [30, 25, 28, 22, 20, 26, 24],
color: '#FF5252',
title: '本月请假',
compareText: '较昨日同期',
chartType: 'line'
},
rate: {
value: '96.8%',
change: '+10%',
isPositive: true,
chartData: [90, 92, 94, 95, 97, 98, 96.8],
color: '#029CD4',
title: '平均出勤率',
compareText: '较昨日同期',
chartType: 'line'
}
});
// 审批数据 - 用于approval组件
@ -185,47 +486,87 @@ const approvalData = ref([
// 今日出勤数据 - 用于todayAttend组件
const todayAttendData = ref({
attendance: {
count: 150,
icon: '/src/assets/demo/qin.png'
},
late: {
count: 5,
icon: '/src/assets/demo/chi.png'
},
earlyLeave: {
count: 2,
icon: '/src/assets/demo/tui.png'
},
absent: {
count: 8,
icon: '/src/assets/demo/que.png'
}
attendance: {
count: 150,
icon: '/src/assets/demo/qin.png'
},
late: {
count: 5,
icon: '/src/assets/demo/chi.png'
},
earlyLeave: {
count: 2,
icon: '/src/assets/demo/tui.png'
},
absent: {
count: 8,
icon: '/src/assets/demo/que.png'
}
});
// 日历数据 - 用于calendar组件
const calendarData = ref({
// 初始化当前日期
today: new Date(),
currentDate: new Date(2025, 8, 27), // 2025年9月27日截图中显示的日期
selectedDate: new Date(2025, 8, 27),
// 初始化当前日期
today: new Date(),
currentDate: new Date(2025, 8, 27), // 2025年9月27日截图中显示的日期
selectedDate: new Date(2025, 8, 27),
// 模拟考勤数据
attendanceData: {
2025: {
9: {
1: 'normal',
4: 'late',
8: 'absent',
10: 'leave',
15: 'normal',
20: 'normal',
25: 'late',
27: 'normal'
}
// 模拟考勤数据
attendanceData: {
2025: {
9: {
1: 'normal',
4: 'late',
8: 'absent',
10: 'leave',
15: 'normal',
20: 'normal',
25: 'late',
27: 'normal'
}
}
}
}
});
// 封装刷新排班数据的方法
const refreshScheduleData = (projectId: string) => {
// 获取排班数据
getscheduleData({
projectId: projectId,
schedulingStartDate: currentMonthDates[0].fullDate,
schedulingEndDate: currentMonthDates[currentMonthDates.length - 1].fullDate,
});
// 获取排班类型
getTypeList();
};
// 监听projectId变化
const projectIdWatcher = watch(
() => userStore.selectedProject?.id,
(newProjectId, oldProjectId) => {
if (newProjectId && newProjectId !== oldProjectId) {
refreshScheduleData(newProjectId);
}
},
{ immediate: false, deep: true }
);
onMounted(() => {
// 刷新排班数据
refreshScheduleData(userStore.selectedProject.id);
// 获取可以排班的人员列表
fetchPaibanRenYuanList(String(userStore.deptId));
});
// 组件卸载时移除监听器
onUnmounted(() => {
if (projectIdWatcher) {
projectIdWatcher();
}
});
</script>
<style scoped lang="scss">
@ -260,11 +601,11 @@ const calendarData = ref({
height: 100%;
}
.calendar-content .el-card > * {
.calendar-content .el-card>* {
margin-bottom: 16px;
}
.calendar-content .el-card > *:last-child {
.calendar-content .el-card>*:last-child {
margin-bottom: 0;
flex: 1;
}
@ -302,7 +643,7 @@ const calendarData = ref({
}
/* 日历卡片内组件间距 */
.calendar-content .el-card > * {
.calendar-content .el-card>* {
margin-bottom: 12px;
}
}

View File

@ -0,0 +1,274 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="排班名称" prop="schedulingName">
<el-input v-model="queryParams.schedulingName" placeholder="请输入排班名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['personnel:schedulingDate:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['personnel:schedulingDate:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['personnel:schedulingDate:remove']">删除</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="schedulingDateList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="排班名称" align="center" prop="schedulingName" />
<el-table-column label="开始时间" align="center" prop="startTime" width="180">
<template #default="scope">
<span>{{scope.row.startTime}}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="180">
<template #default="scope">
<span>{{ scope.row.endTime}}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['personnel:schedulingDate:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['personnel:schedulingDate:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改运维-排班时间类型对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="schedulingDateFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="排班名称" prop="schedulingName">
<el-input v-model="form.schedulingName" placeholder="请输入排班名称" />
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-time-select clearable
v-model="form.startTime"
value-format="HH:mm:ss"
step="00:10:00"
start="00:00"
end="23:59"
placeholder="请选择开始时间">
</el-time-select>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-time-select clearable
v-model="form.endTime"
value-format="HH:mm:ss"
step="00:10:00"
start="00:00"
end="23:59"
placeholder="请选择结束时间">
</el-time-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="SchedulingDate" lang="ts">
import { ref, reactive, toRefs, onMounted, onUnmounted, watch, getCurrentInstance } from 'vue';
import { listSchedulingDate, getSchedulingDate, delSchedulingDate, addSchedulingDate, updateSchedulingDate } from '@/api/renyuan/schedulingDate';
import { SchedulingDateVO, SchedulingDateQuery, SchedulingDateForm } from '@/api/renyuan/schedulingDate/types';
// 导入用户store
import { useUserStore } from '@/store/modules/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户store
const userStore = useUserStore();
const schedulingDateList = ref<SchedulingDateVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const schedulingDateFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: SchedulingDateForm = {
id: undefined,
schedulingName: undefined,
startTime: undefined,
endTime: undefined,
projectId: undefined,
}
const data = reactive<PageData<SchedulingDateForm, SchedulingDateQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
schedulingName: undefined,
startTime: undefined,
endTime: undefined,
projectId: undefined,
params: {
}
},
rules: {
schedulingName: [
{ required: true, message: "排班名称不能为空", trigger: "blur" }
],
startTime: [
{ required: true, message: "开始时间不能为空", trigger: "blur" }
],
endTime: [
{ required: true, message: "结束时间不能为空", trigger: "blur" }
]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询运维-排班时间类型列表 */
const getList = async () => {
loading.value = true;
const res = await listSchedulingDate(queryParams.value);
schedulingDateList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
schedulingDateFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: SchedulingDateVO[]) => {
ids.value = selection.map(item => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
// 显式设置projectId为当前选中的项目ID
if (userStore.selectedProject && userStore.selectedProject.id) {
form.value.projectId = userStore.selectedProject.id;
}
dialog.visible = true;
dialog.title = "添加运维-排班时间类型";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: SchedulingDateVO) => {
reset();
const _id = row?.id || ids.value[0]
const res = await getSchedulingDate(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改运维-排班时间类型";
}
/** 提交按钮 */
const submitForm = () => {
schedulingDateFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateSchedulingDate(form.value).finally(() => buttonLoading.value = false);
} else {
await addSchedulingDate(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: SchedulingDateVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除运维-排班时间类型编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
await delSchedulingDate(_ids);
proxy?.$modal.msgSuccess("删除成功");
await getList();
}
// 监听用户选择的项目变化
watch(() => userStore.selectedProject, (newProject) => {
if (newProject && newProject.id) {
queryParams.value.projectId = newProject.id;
// 只在新增表单时设置projectId编辑表单保留原有值
if (!form.value.id) {
form.value.projectId = newProject.id;
}
// 调用getList刷新数据
getList();
}
}, { immediate: true, deep: true });
onMounted(() => {
getList();
});
// 组件卸载时清空projectId
onUnmounted(() => {
queryParams.value.projectId = undefined;
form.value.projectId = undefined;
});
</script>

View File

@ -1,7 +1,6 @@
<template>
<div class="chart-container">
<!--组件温度 图表内容区域 -->
<div ref="chartRef" class="chart-content"></div>
</div>
</template>

View File

@ -5,35 +5,35 @@
<template #header>
<h3>基础信息</h3>
</template>
<el-form :model="basicInfo" label-width="120px">
<el-form :model="detailInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="订单编号">
<el-input v-model="basicInfo.orderNo" disabled />
<el-form-item label="采购单号">
<el-input v-model="detailInfo.id" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="创建时间">
<el-input v-model="basicInfo.createTime" disabled />
<el-input v-model="detailInfo.createTime" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="经办人">
<el-input v-model="basicInfo.handler" disabled />
<el-input v-model="detailInfo.jingbanrenName" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属部门">
<el-select v-model="basicInfo.dept" placeholder="请选择">
<el-select v-model="detailInfo.caigouDanweiName" placeholder="请选择">
<el-option label="运维部" value="运维部" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购类型">
<el-select v-model="basicInfo.purchaseType" placeholder="请选择">
<el-select v-model="detailInfo.caigouType" placeholder="请选择">
<el-option label="项目业务" value="项目业务" />
</el-select>
</el-form-item>
@ -50,32 +50,23 @@
<template #header>
<h3>供应商信息</h3>
</template>
<el-form :model="supplierInfo" label-width="120px">
<el-form :model="detailInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="供应商单位">
<el-select v-model="supplierInfo.supplierName" placeholder="请选择">
<el-select v-model="detailInfo.gonyingshangId" placeholder="请选择">
<el-option label="AAAA精密仪器制造有限公司" value="AAAA精密仪器制造有限公司" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出货时间">
<el-select v-model="supplierInfo.deliveryTime" placeholder="请选择">
<el-select v-model="detailInfo.chouhuoTime" placeholder="请选择">
<el-option label="2年零4个月" value="2年零4个月" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="审批备注" prop="remark">
<el-input v-model="supplierInfo.remark" :rows="1" placeholder="请输入审批备注"
style="border: 1px solid red;color: red;" readonly value="1. 出货时间较长" />
<!-- <div class="error-tip">1. 出货时间较长</div> -->
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
@ -84,19 +75,14 @@
<template #header>
<h3>产品信息</h3>
</template>
<el-table :data="productInfo.tableData" border style="width: 100%">
<el-table-column prop="productName" label="产品名称" />
<el-table-column prop="productModel" label="产品型号" />
<el-table-column prop="productPrice" label="产品单价" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="buyQuantity" label="购买数量" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="usage" label="用途" />
<el-table-column prop="total" label="合计" />
<el-table :data="detailInfo.opsCaigouPlanChanpinVos" border style="width: 100%">
<el-table-column prop="chanpinName" label="产品名称" />
<el-table-column prop="chanpinType" label="产品型号" />
<el-table-column prop="chanpinMonovalent" label="产品单价" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="goumaiNumber" label="购买数量" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="yontu" label="用途" />
<el-table-column prop="totalPrice" label="合计" />
</el-table>
<el-form-item label="审批备注" style="margin-top: 10px">
<el-input v-model="productInfo.remark" :rows="1" placeholder="请输入审批备注"
style="border: 1px solid red;color: red;" readonly value="2. 单价高于市场价3.采购数量需重新评估" />
<!-- <div class="error-tip">2. 单价高于市场价3.采购数量需重新评估</div> -->
</el-form-item>
</el-card>
<!-- 合同条款 -->
@ -108,28 +94,19 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="付款条件">
<el-select v-model="contractInfo.paymentCondition" placeholder="请选择">
<el-select v-model="detailInfo.fukuantiaojian" placeholder="请选择">
<el-option label="银行卡" value="银行卡" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发票开具方式">
<el-select v-model="contractInfo.invoiceWay" placeholder="请选择">
<el-select v-model="detailInfo.fapiaoKjfs" placeholder="请选择">
<el-option label="请选择" value="请选择" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="审批备注" prop="remark">
<el-input v-model="contractInfo.remark" placeholder="请输入审批备注"
style="border: 1px solid red;color: red;" readonly value="4. 付款方式未标明5.发票开具方式未标明" />
<!-- <div class="error-tip">4. 付款方式未标明5.发票开具方式未标明</div> -->
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
@ -157,9 +134,39 @@
</div>
</template>
<script setup>
import { ref } from 'vue';
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance, toRefs } from 'vue';
import { useRoute } from 'vue-router';
import type { ComponentInternalInstance } from 'vue';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wz_invoicing_way, wz_payment_terms, wz_purchase_type, wz_contract_type, wz_caigou_examine } = toRefs<any>(proxy?.useDict('wz_invoicing_way', 'wz_payment_terms', 'wz_purchase_type', 'wz_contract_type', 'wz_caigou_examine'));
import { caigouPlanDetail } from '@/api/wuziguanli/caigouPlan';
import { CaigouPlanVO, CaigouPlanQuery, CaigouPlanForm } from '@/api/wuziguanli/caigouPlan/types';
// 存储计划详情数据
const detailInfo = ref<CaigouPlanVO>({} as CaigouPlanVO);
// 存储计划编号
const id = ref('');
const getDetailInfo = async () => {
const res = await caigouPlanDetail(id.value);
if (res.code === 200) {
detailInfo.value = res.data;
console.log(detailInfo.value);
}
}
onMounted(() => {
// 接收路由参数
id.value = route.query.id as string;
getDetailInfo();
});
// 基础信息数据
const basicInfo = ref({
orderNo: '0035455',

View File

@ -0,0 +1,261 @@
<template>
<div class="approval-form">
<!-- 基础信息 -->
<el-card class="card" shadow="hover">
<template #header>
<h3>基础信息</h3>
</template>
<el-form :model="basicInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="订单编号">
<el-input v-model="basicInfo.orderNo" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="创建时间">
<el-input v-model="basicInfo.createTime" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="经办人">
<el-input v-model="basicInfo.handler" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属部门">
<el-select v-model="basicInfo.dept" placeholder="请选择">
<el-option label="运维部" value="运维部" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购类型">
<el-select v-model="basicInfo.purchaseType" placeholder="请选择">
<el-option label="项目业务" value="项目业务" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="申请原因">
<el-input v-model="basicInfo.applyReason" type="textarea" :rows="2" placeholder="请输入申请原因" />
</el-form-item>
</el-form>
</el-card>
<!-- 供应商信息 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>供应商信息</h3>
</template>
<el-form :model="supplierInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="供应商单位">
<el-select v-model="supplierInfo.supplierName" placeholder="请选择">
<el-option label="AAAA精密仪器制造有限公司" value="AAAA精密仪器制造有限公司" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出货时间">
<el-select v-model="supplierInfo.deliveryTime" placeholder="请选择">
<el-option label="2年零4个月" value="2年零4个月" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 产品信息 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>产品信息</h3>
</template>
<el-table :data="productInfo.tableData" border style="width: 100%">
<el-table-column prop="productName" label="产品名称" />
<el-table-column prop="productModel" label="产品型号" />
<el-table-column prop="productPrice" label="产品单价" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="buyQuantity" label="购买数量" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="usage" label="用途" />
<el-table-column prop="total" label="合计" />
</el-table>
</el-card>
<!-- 合同条款 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>合同条款</h3>
</template>
<el-form :model="contractInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="付款条件">
<el-select v-model="contractInfo.paymentCondition" placeholder="请选择">
<el-option label="银行卡" value="银行卡" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发票开具方式">
<el-select v-model="contractInfo.invoiceWay" placeholder="请选择">
<el-option label="请选择" value="请选择" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 附件 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>附件</h3>
</template>
<el-upload class="upload-demo" action="#" :file-list="fileList" :auto-upload="false"
:on-preview="handlePreview">
<el-table :data="fileList" border style="width: 100%">
<el-table-column prop="name" label="文件名" width="300" />
<el-table-column prop="size" label="大小" width="100" />
<el-table-column label="操作" width="100">
<template #default="scope">
<!-- <el-link type="primary" @click="handlePreview(scope.row)"> -->
<el-link type="primary">
预览
</el-link>
</template>
</el-table-column>
</el-table>
</el-upload>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance, toRefs } from 'vue';
import { useRoute } from 'vue-router';
import type { ComponentInternalInstance } from 'vue';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wz_invoicing_way, wz_payment_terms, wz_purchase_type, wz_contract_type, wz_caigou_examine } = toRefs<any>(proxy?.useDict('wz_invoicing_way', 'wz_payment_terms', 'wz_purchase_type', 'wz_contract_type', 'wz_caigou_examine'));
import { caigouPlanDetail } from '@/api/wuziguanli/caigouPlan';
import { CaigouPlanVO, CaigouPlanQuery, CaigouPlanForm } from '@/api/wuziguanli/caigouPlan/types';
// 存储计划编号
const id = ref('');
const getDetailInfo = async () => {
const res = await caigouPlanDetail(id.value);
if (res.code === 200) {
console.log(res);
}
}
onMounted(() => {
// 接收路由参数
id.value = route.query.id as string;
getDetailInfo();
});
// 基础信息数据
const basicInfo = ref({
orderNo: '0035455',
createTime: '2023-11-02 16:32',
handler: '李四',
dept: '运维部',
purchaseType: '项目业务',
applyReason:
'随着业务拓展光伏电站业务负责增加现有设备已运行5年部分出现效率下降情况。为保证电站正常运行计划采购一批新的逆变器替换老旧设备并补充备件库存。',
});
// 供应商信息数据
const supplierInfo = ref({
supplierName: 'AAAA精密仪器制造有限公司',
deliveryTime: '2年零4个月',
remark: '',
});
// 产品信息数据
const productInfo = ref({
tableData: [
{
productName: 'AAABBBCCC',
productModel: '15-42',
productPrice: 500,
buyQuantity: 10,
usage: '组件',
total: 5000,
},
],
remark: '',
});
// 合同条款数据
const contractInfo = ref({
paymentCondition: '银行卡',
invoiceWay: '请选择',
remark: '',
});
// 附件数据
const fileList = ref([
{
name: 'MWwwwww.jpg',
size: '30kb',
url: '',
},
{
name: '231234124w.zip',
size: '50kb',
url: '',
},
{
name: '12451asdas.doc',
size: '80kb',
url: '',
},
{
name: '21seasda.xls',
size: '29kb',
url: '',
},
{
name: '12kjaklskw.png',
size: '16kb',
url: '',
},
]);
// 预览文件
const handlePreview = (file) => {
console.log('预览文件:', file);
// 实际场景可在这里处理文件预览逻辑,如打开新窗口等
};
</script>
<style scoped>
.approval-form {
padding: 20px;
}
.card {
border-radius: 8px;
}
.error-tip {
color: red;
font-size: 12px;
margin-top: 5px;
}
::v-deep(.el-input__inner) {
color: red;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="inventoryManagement">
<!-- <TitleComponent title="出入库单管理" subtitle="管理光伏和风电设备备品备件的出入库记录" /> -->
<el-row gutter="20">
<el-row :gutter="20">
<el-col :span="16" class="list" style="flex-grow: 1;display: flex;">
<el-card style="border-radius: 10px;height: 100%;display: flex;flex-direction: column;flex: 1;">
<div style="height: 100%;flex: 1;">
@ -13,46 +13,93 @@
</div>
</div>
<div class="content" style="height: 100%;flex: 1;">
<div class="menu">
<el-input placeholder="请输入单据编号"></el-input>
<el-select placeholder="请选择单据类型"></el-select>
<el-select placeholder="请选择设备类型"></el-select>
<el-select placeholder="请选择状态"></el-select>
<el-select placeholder="请选择日期范围"></el-select>
<el-button icon="search" type="primary">搜索</el-button>
<el-button icon="refresh">重置</el-button>
</div>
<div style="margin-top: 10px;">
<el-button type="primary" @click="dialogVisible = true;">+{{ type === 'chuku' ? '添加出库单'
<!-- 第一排四个输入项 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="单据编号" prop="danjvNumber">
<el-input v-model="queryParams.danjvNumber" placeholder="请输入单据编号"
clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备类型" prop="shebeiType">
<el-select v-model="queryParams.shebeiType" placeholder="请选择设备类型"
clearable>
<el-option v-for="dict in wz_device_type" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="审核状态" prop="auditStatus">
<el-select v-model="queryParams.auditStatus" placeholder="请选择审核状态"
clearable>
<el-option v-for="dict in shenheStatus" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="queryParams.startDate" type="date"
placeholder="请选择开始日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 100%" />
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="queryParams.endDate" type="date"
placeholder="请选择结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 100%" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search"
@click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<div style="margin-top: 10px; display: flex; justify-content: flex-end;">
<el-button type="primary" @click="handleAdd">+{{ type === 'chuku' ? '添加出库单'
: '添加入库单' }}</el-button>
</div>
<el-table :data="tableData" border style="width: 100%;margin-top: 15px;height: 1000px;">
<el-table-column prop="formNumber" label="单据编号" />
<el-table-column prop="equipmentType" label="设备类型" />
<el-table-column prop="handler" label="经手人" />
<el-table-column prop="operationTime" label="操作时间" />
<el-table-column prop="totalQuantity" label="总数量" />
<el-table-column label="状态">
<el-table v-loading="loading" border :data="churukudanList"
style="width: 100%;margin-top: 15px;">
<el-table-column label="单据编号" align="center" prop="danjvNumber" />
<el-table-column label="设备类型" align="center" prop="shebeiType">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">
{{ scope.row.status }}
<span>{{ getTagLabel(wz_device_type, scope.row.shebeiType) }}</span>
</template>
</el-table-column>
<el-table-column label="经手人" align="center" prop="jingshourenName" />
<el-table-column label="操作时间" align="center" prop="updateTime" />
<el-table-column label="总数量" align="center" prop="zonNumber" width="80px" />
<el-table-column label="审核状态" align="center" prop="shenheStatus">
<template #default="scope">
<el-tag :type="getTagType(shenheStatus, scope.row.shenheStatus)" as="span">
{{ getTagLabel(shenheStatus, scope.row.shenheStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<el-table-column label="单据类型" align="center" prop="danjvType">
<template #default="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
<el-tag :type="getTagType(danjvType, scope.row.danjvType)">
{{ getTagLabel(danjvType, scope.row.danjvType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="handleUpdate(scope.row)"
v-hasPermi="['personnel:churukudan:edit']">修改</el-button>
<el-button link type="primary" @click="handleDetail(scope.row)"
v-hasPermi="['personnel:churukudan:query']">详情</el-button>
<el-button link type="primary" @click="handleDelete(scope.row)"
v-hasPermi="['personnel:churukudan:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="tool">
<div class="pagination-section">
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="currentPage" :page-sizes="[10, 20, 30, 40]" :page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="total" background>
</el-pagination>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</div>
</div>
@ -77,37 +124,67 @@
</el-card>
</el-col>
</el-row>
<el-dialog v-model="dialogVisible" :title="type === 'chuku' ? '添加出库单' : '添加入库单'" width="500">
<el-form :rules="rules" ref="formRef" label-width="100">
<el-form-item label="单据编号" prop="formNumber">
<el-input v-model="form.formNumber" placeholder="请输入单据编号" />
</el-form-item>
<el-form-item label="设备类型" prop="equipmentType">
<el-select v-model="form.equipmentType" placeholder="请选择设备类型">
<el-option label="设备类型1" value="1" />
<el-option label="设备类型2" value="2" />
<!-- 添加或修改运维-物资-出入库单管理对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="churukudanFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="单据类型" prop="danjvType">
<el-select v-model="form.danjvType" placeholder="请选择单据类型">
<el-option v-for="dict in danjvType" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="入库数量" prop="totalQuantity">
<el-input v-model="form.totalQuantity" placeholder="请输入总数量" />
<el-form-item label="单据编号" prop="danjvNumber">
<el-input v-model="form.danjvNumber" placeholder="请输入单据编号" />
</el-form-item>
<el-form-item label="经手人" prop="handler">
<el-input v-model="form.handler" placeholder="请输入经手人" />
<el-form-item label="设备类型" prop="shebeiType">
<el-select v-model="form.shebeiType" placeholder="请选择设备类型">
<el-option v-for="dict in wz_device_type" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
<!-- 联系电话 -->
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
<el-form-item label="经手人id" prop="jingshourenId">
<el-input v-model="form.jingshourenId" placeholder="请输入经手人id" />
</el-form-item>
<el-form-item label="经手人" prop="jingshourenName">
<el-input v-model="form.jingshourenName" placeholder="请输入经手人" />
</el-form-item>
<el-form-item label="联系电话" prop="contactNumber">
<el-input v-model="form.contactNumber" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="总数量" prop="zonNumber">
<el-input v-model="form.zonNumber" placeholder="请输入总数量" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="dialogVisible = false">
保存
</el-button>
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="出入库单详情" v-model="detailVisible" width="500px" append-to-body>
<el-descriptions :column="1" border>
<el-descriptions-item label="单据类型">{{ getTagLabel(danjvType, detailData.danjvType)
}}</el-descriptions-item>
<el-descriptions-item label="单据编号">{{ detailData.danjvNumber }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ getTagLabel(wz_device_type, detailData.shebeiType)
}}</el-descriptions-item>
<el-descriptions-item label="经手人">{{ detailData.jingshourenName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailData.contactNumber }}</el-descriptions-item>
<el-descriptions-item label="总数量">{{ detailData.zonNumber }}</el-descriptions-item>
<el-descriptions-item label="审核状态">
<dict-tag :options="shenheStatus" :value="detailData.shenheStatus"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style scoped>
@ -149,8 +226,6 @@
}
.menu {
display: flex;
gap: 20px;
background-color: #F2F2F2;
padding: 20px;
}
@ -212,69 +287,368 @@
}
}
/* 详情弹窗样式 */
.detail-container {
padding: 10px 0;
}
.detail-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.detail-label {
font-weight: 500;
color: #606266;
width: 120px;
}
.detail-value {
color: #303133;
flex: 1;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
padding: 12px 0;
}
::v-deep(.el-card__body) {
height: 100%;
}
</style>
<script setup>
<script setup lang="ts">
import SystemInfo from './components/SystemInfo.vue';
import DataAnalysis from './components/DataAnalysis.vue';
const type = ref('chuku');
const form = ref({
formNumber: '',
equipmentType: '',
handler: '',
totalQuantity: ''
import { ref, computed } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { listChurukudan, getChurukudan, delChurukudan, addChurukudan, updateChurukudan, getChuRuKuCountBar } from '@/api/wuziguanli/churuku/index';
import { ChurukudanVO, ChurukudanQuery, ChurukudanForm } from '@/api/wuziguanli/churuku/types';
const { wz_device_type } = toRefs<any>(proxy?.useDict('wz_device_type'));
import { getCurrentMonthDates } from '@/utils/getDate';
const currentMonthDates = getCurrentMonthDates();
// 导入用户store
import { useUserStore } from '@/store/modules/user';
// 获取用户store
const userStore = useUserStore();
const churukudanList = ref<ChurukudanVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const total = ref(0);
// 单据类型切换变量 - 默认出库单
const type = ref<string>('chuku');
/** 切换单据类型 */
const changeType = (newType: string) => {
type.value = newType;
// 更新查询参数
queryParams.value.pageNum = 1;
queryParams.value.danjvType = newType === 'chuku' ? '1' : '2';
// 重新加载数据
getList();
}
// 单据类型
const danjvType = ref([
{
value: '1',
label: '出库单',
type: 'primary'
},
{
value: '2',
label: '入库单',
type: 'success'
}
]);
// 审核类型
const shenheStatus = ref([
{
value: 'draft',
label: '草稿',
type: 'primary'
},
{
value: 'waiting',
label: '待审核',
type: 'warning',
},
{
value: 'finish',
label: '已完成',
type: 'success'
}
])
// 根据字典数组和值获取标签类型
const getTagType = (dictArray: any[], value: any): string => {
if (!dictArray || !value) return '';
const item = dictArray.find(item => item.value === value);
return item?.type || '';
}
// 根据字典数组和值获取标签文本
const getTagLabel = (dictArray: any[], value: any): string => {
if (!dictArray || !value) return '';
const item = dictArray.find(item => item.value === value);
return item?.label || value;
}
const queryFormRef = ref<ElFormInstance>();
const churukudanFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const changeType = (newType) => {
type.value = newType;
};
const dialogVisible = ref(false);
const tableData = computed(() => {
return Array.from({ length: 50 }, (_, index) => ({
formNumber: 'IN-2023-0615-001',
equipmentType: '光伏设备',
handler: '李仓库',
operationTime: '2023-06-15 09:23',
totalQuantity: 120,
// 待审核,已完成,已取消 随机生成
status: Math.random() > 0.5 ? '待审核' : Math.random() > 0.5 ? '已完成' : '已取消'
}))
})
// 当前页码
const currentPage = ref(1);
// 每页条数 - 与分页控件默认值保持一致
const pageSize = ref(10);
// 总条数 - 从原始数据计算得出
const total = ref(tableData.value.length);
const pagedTableData = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value;
const endIndex = startIndex + pageSize.value;
return tableData.value.slice(startIndex, endIndex);
});
// 表单校验规则
const rules = ref({
formNumber: [{ required: true, message: '请输入表单编号', trigger: 'blur' }],
equipmentType: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
handler: [{ required: true, message: '请输入经手人', trigger: 'blur' }],
totalQuantity: [{ required: true, message: '请输入入库数量', trigger: 'blur' }],
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
});
// 表单引用
const formRef = ref(null);
// 当前页码改变
const handleCurrentChange = (val) => {
currentPage.value = val;
};
const getStatusTagType = (status) => {
if (status === '已完成') {
return 'success'
} else if (status === '待审核') {
return 'warning'
} else if (status === '已取消') {
return 'danger'
}
return ''
// 详情弹窗显示状态
const detailVisible = ref(false);
// 详情数据
const detailData = ref<ChurukudanVO>({} as ChurukudanVO);
const initFormData: ChurukudanForm = {
id: undefined,
projectId: undefined,
danjvNumber: undefined,
shebeiType: undefined,
jingshourenId: undefined,
jingshourenName: undefined,
contactNumber: undefined,
zonNumber: undefined,
shenheStatus: undefined,
danjvType: undefined,
updateTime: undefined,
auditStatus: undefined,
}
const data = reactive<PageData<ChurukudanForm, ChurukudanQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
danjvNumber: undefined,
shebeiType: undefined,
shenheStatus: undefined,
startDate: undefined,
endDate: undefined,
auditStatus: undefined,
danjvType: '1', // 默认显示出库单
params: {
}
},
rules: {
shebeiType: [
{ required: true, message: "设备类型不能为空", trigger: "change" }
],
jingshourenId: [
{ required: true, message: "经手人id不能为空", trigger: "blur" }
],
jingshourenName: [
{ required: true, message: "经手人不能为空", trigger: "blur" }
],
zonNumber: [
{ required: true, message: "总数量不能为空", trigger: "blur" }
],
danjvType: [
{ required: true, message: "单据状态不能为空", trigger: "change" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询运维-物资-出入库单管理列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listChurukudan(queryParams.value);
churukudanList.value = res.rows || [];
total.value = res.total || 0;
} catch (error) {
console.error('获取出入库单列表失败:', error);
proxy?.$modal.msgError("获取数据失败,请稍后重试");
churukudanList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
churukudanFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
// 检查日期范围筛选条件
if ((queryParams.value.startDate && !queryParams.value.endDate) ||
(!queryParams.value.startDate && queryParams.value.endDate)) {
proxy?.$modal.msgWarning("时间范围筛选必须同时选择开始日期和结束日期");
return;
}
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
if (userStore.selectedProject && userStore.selectedProject.id) {
form.value.projectId = userStore.selectedProject.id;
}
// 根据当前选择的类型自动设置单据类型
form.value.danjvType = type.value === 'chuku' ? '1' : '2';
dialog.visible = true;
dialog.title = type.value === 'chuku' ? "添加出库单" : "添加入库单";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: ChurukudanVO) => {
reset();
const _id = row?.id || ids.value[0];
if (!_id) {
proxy?.$modal.msgWarning("请选择要修改的数据");
return;
}
try {
const res = await getChurukudan(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改运维-物资-出入库单管理";
} catch (error) {
console.error('获取出入库单详情失败:', error);
proxy?.$modal.msgError("获取数据失败,请稍后重试");
}
}
/** 提交按钮 */
const submitForm = () => {
churukudanFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateChurukudan(form.value);
proxy?.$modal.msgSuccess("修改成功");
} else {
await addChurukudan(form.value);
proxy?.$modal.msgSuccess("添加成功");
}
dialog.visible = false;
await getList();
} catch (error) {
console.error('保存出入库单失败:', error);
proxy?.$modal.msgError("操作失败,请稍后重试");
} finally {
buttonLoading.value = false;
}
}
});
}
/** 详情按钮操作 */
const handleDetail = async (row?: ChurukudanVO) => {
if (!row?.id) {
proxy?.$modal.msgWarning("请选择要查看详情的数据");
return;
}
try {
const res = await getChurukudan(row.id);
detailData.value = res.data || {} as ChurukudanVO;
detailVisible.value = true;
} catch (error) {
console.error('获取出入库单详情失败:', error);
proxy?.$modal.msgError("获取详情失败,请稍后重试");
}
}
/** 删除按钮操作 */
const handleDelete = async (row?: ChurukudanVO) => {
const _ids = row?.id || ids.value;
if (!_ids || (_ids instanceof Array && _ids.length === 0)) {
proxy?.$modal.msgWarning("请选择要删除的数据");
return;
}
try {
const confirmed = await proxy?.$modal.confirm('是否确认删除运维-物资-出入库单管理编号为"' + _ids + '"的数据项?');
if (!confirmed) return;
loading.value = true;
await delChurukudan(_ids);
proxy?.$modal.msgSuccess("删除成功");
await getList();
} catch (error) {
console.error('删除出入库单失败:', error);
proxy?.$modal.msgError("删除失败,请稍后重试");
} finally {
loading.value = false;
}
}
// 柱状图数据获取
const fetchChuRuKuCountBarData = async () => {
if (!queryParams.value.projectId) {
return;
}
let data = {
projectId: queryParams.value.projectId,
startDate: currentMonthDates[0].fullDate,
endDate: currentMonthDates[currentMonthDates.length - 1].fullDate,
}
try {
const res = await getChuRuKuCountBar(data);
console.log(res);
// 这里可以添加数据处理和图表更新的逻辑
} catch (error) {
console.error('获取柱状图数据失败:', error);
// 可以选择是否显示错误提示根据UI需求决定
// proxy?.$modal.msgError("获取统计数据失败");
}
}
// 监听用户选择的项目变化
watch(() => userStore.selectedProject, (newProject) => {
if (newProject && newProject.id) {
queryParams.value.projectId = newProject.id;
// 只在新增表单时设置projectId编辑表单保留原有值
if (!form.value.id) {
form.value.projectId = newProject.id;
}
// 调用getList刷新数据
getList();
fetchChuRuKuCountBarData();
}
}, { immediate: true, deep: true });
onMounted(() => {
getList();
fetchChuRuKuCountBarData();
});
// 组件卸载时清空projectId
onUnmounted(() => {
queryParams.value.projectId = undefined;
form.value.projectId = undefined;
});
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="procurementPlan">
<el-row gutter="20">
<el-row :gutter="20">
<el-col :span="13">
<el-card>
<div style="display: flex;align-items: center;height: 120px;justify-content: space-around;">
@ -79,37 +79,43 @@
<!-- 标签页导航 -->
<div class="tabs">
<el-button :type="activeTab === 'pending' ? 'primary' : ''"
@click="changeTab('pending')">待审批</el-button>
<el-button :type="activeTab === 'procuring' ? 'primary' : ''"
@click="changeTab('procuring')">采购中</el-button>
<el-badge :value="5" type="danger">
<!-- <el-badge :value="pendingCount" type="warning">
<el-button :type="activeTab === 'pending' ? 'primary' : ''"
@click="changeTab('pending')">待审批</el-button>
</el-badge>
<el-badge :value="purchasingCount" type="info">
<el-button :type="activeTab === 'purchasing' ? 'primary' : ''"
@click="changeTab('purchasing')">采购中</el-button>
</el-badge>
<el-badge :value="rejectedCount" type="danger">
<el-button :type="activeTab === 'rejected' ? 'primary' : ''"
@click="changeTab('rejected')">
未通过
</el-button>
</el-badge>
<el-button :type="activeTab === 'approved' ? 'primary' : ''"
@click="changeTab('approved')">已通过</el-button>
<el-button :type="activeTab === 'completed' ? 'primary' : ''"
@click="changeTab('completed')">已完成</el-button>
<el-badge :value="approvedCount" type="primary">
<el-button :type="activeTab === 'approved' ? 'primary' : ''"
@click="changeTab('approved')">已通过</el-button>
</el-badge>
<el-badge :value="completedCount" type="success">
<el-button :type="activeTab === 'completed' ? 'primary' : ''"
@click="changeTab('completed')">已完成</el-button>
</el-badge> -->
</div>
<!-- 表格 -->
<el-table :data="tableData" border style="width: 100%;margin-top: 15px;">
<el-table-column type="selection" width="55" />
<el-table-column prop="planNumber" label="计划编号" />
<el-table-column prop="planName" label="计划名称" />
<el-table-column prop="equipmentType" label="设备类型" />
<el-table-column prop="requestDept" label="申请部门" />
<el-table-column prop="applicant" label="申请人" />
<el-table-column prop="requestDate" label="申请日期" />
<el-table-column prop="estimatedAmount" label="预计金额" />
<el-table-column label="状态">
<el-table :data="caigouPlanList" border style="width: 100%;margin-top: 15px;">
<el-table-column label="计划编号" align="center" prop="jihuaBianhao" />
<el-table-column label="计划名称" align="center" prop="jihuaName" />
<el-table-column label="申请部门" align="center" prop="caigouDanweiName" />
<el-table-column label="申请人" align="center" prop="jingbanrenName" />
<el-table-column prop="createTime" label="申请日期" align="center" />
<el-table-column label="预计金额" align="center" prop="yujiJine" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)">{{ scope.row.status }}</el-tag>
<dict-tag :options="wz_caigou_examine" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="80">
<el-table-column label="操作" fixed="right" width="80" align="center">
<template #default="scope">
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
</template>
@ -118,9 +124,8 @@
<!-- 分页 -->
<div class="pagination-section">
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="currentPage" :page-sizes="[10, 20, 30, 40]" :page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="total" background />
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</div>
</el-card>
@ -131,53 +136,40 @@
<!-- 基础信息 -->
<div class="form-section">
<h3>基础信息</h3>
<!-- 输入框行 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="订单编号">
<el-input v-model="newProcurementForm.planNumber" disabled value="PLAN-2023-0615-003" />
<el-col :span="12">
<el-form-item label="计划名称">
<el-input v-model="form.jihuaName" placeholder="请填写计划名称" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="创建时间">
<el-input v-model="newProcurementForm.createTime" disabled value="2023-11-02-16:32" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="采购单位">
<el-input v-model="newProcurementForm.procurementUnit" disabled value="大连好果汁有限公司" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="经办人">
<el-input v-model="newProcurementForm.handler" disabled value="李四" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="合同类型">
<el-select v-model="newProcurementForm.contractType" placeholder="请选择">
<el-option label="请选择" value="" />
<!-- 可以添加更多选项 -->
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="采购类型">
<el-select v-model="newProcurementForm.procurementType" placeholder="请选择">
<el-option label="请选择" value="" />
<!-- 可以添加更多选项 -->
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="仓库地址">
<el-select v-model="newProcurementForm.contractAddress" placeholder="请选择">
<el-option label="请选择" value="" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="12">
<el-form-item label="合同名称">
<el-input v-model="newProcurementForm.contractName" placeholder="请填写" />
<el-input v-model="form.hetonName" placeholder="请填写合同名称" />
</el-form-item>
</el-col>
</el-row>
<!-- 下拉框行 -->
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="合同类型">
<el-select v-model="form.hetonType" placeholder="请选择">
<el-option v-for="option in wz_contract_type" :key="option.value"
:label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="采购类型">
<el-select v-model="form.caigouType" placeholder="请选择">
<el-option v-for="option in wz_purchase_type" :key="option.value"
:label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="仓库地址">
<el-input v-model="form.cangkuUrl" placeholder="请输入仓库地址" />
</el-form-item>
</el-col>
</el-row>
@ -189,18 +181,19 @@
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="供应商单位">
<el-select v-model="newProcurementForm.supplierUnit" placeholder="请选择">
<el-option label="请选择" value="" />
<!-- 可以添加更多选项 -->
<el-select v-model="form.danwei" placeholder="请选择">
<!-- <el-option v-for="option in supplierList" :key="option.value" :label="option.label"
:value="option.value" /> -->
<el-option label="供应商1" value="供应商1" />
<el-option label="供应商1" value="供应商1" />
<el-option label="供应商1" value="供应商1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="送货时间">
<el-select v-model="newProcurementForm.deliveryTime" placeholder="请选择">
<el-option label="请选择" value="" />
<!-- 可以添加更多选项 -->
</el-select>
<el-date-picker v-model="form.chuhuoTime" type="date" placeholder="请选择送货日期"
value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
@ -209,30 +202,32 @@
<!-- 产品信息 -->
<div class="form-section">
<h3>产品信息</h3>
<el-table :data="newProcurementForm.products" border style="width: 100%">
<el-table-column prop="productName" label="产品名称">
<el-table :data="form.opsCaigouPlanChanpinBos" border style="width: 100%">
<el-table-column prop="chanpinName" label="产品名称">
<template #default="scope">
<el-input v-model="scope.row.productName" placeholder="请填写" />
<el-input v-model="scope.row.chanpinName" placeholder="请填写" />
</template>
</el-table-column>
<el-table-column prop="productModel" label="产品型号">
<el-table-column prop="chanpinType" label="产品型号">
<template #default="scope">
<el-input v-model="scope.row.productModel" placeholder="请填写" />
<el-input v-model="scope.row.chanpinType" placeholder="请填写" />
</template>
</el-table-column>
<el-table-column prop="productPrice" label="产品单价">
<el-table-column prop="chanpinMonovalent" label="产品单价">
<template #default="scope">
<el-input v-model="scope.row.productPrice" placeholder="请填写" type="number" />
<el-input v-model="scope.row.chanpinMonovalent" placeholder="请填写" type="number"
@change="calculateTotalPrice(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="purchaseQuantity" label="购买数量">
<el-table-column prop="goumaiNumber" label="购买数量">
<template #default="scope">
<el-input v-model="scope.row.purchaseQuantity" placeholder="请填写" type="number" />
<el-input v-model="scope.row.goumaiNumber" placeholder="请填写" type="number"
@change="calculateTotalPrice(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="unit" label="单位">
<el-table-column prop="danwei" label="单位">
<template #default="scope">
<el-input v-model="scope.row.unit" placeholder="请填写" />
<el-input v-model="scope.row.danwei" placeholder="请填写" />
</template>
</el-table-column>
<el-table-column prop="totalPrice" label="合计" :formatter="calculateTotalPrice">
@ -243,7 +238,7 @@
<el-table-column label="操作" fixed="right" width="80">
<template #default="scope">
<el-button type="text" @click="removeProduct(scope.$index)"
:disabled="newProcurementForm.products.length <= 1">删除</el-button>
:disabled="form.opsCaigouPlanChanpinBos.length <= 1">删除</el-button>
</template>
</el-table-column>
</el-table>
@ -255,18 +250,18 @@
<h3>合同条款</h3>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="付款条件">
<el-select v-model="newProcurementForm.paymentTerms" placeholder="请选择">
<el-option label="请选择" value="" />
<!-- 可以添加更多选项 -->
<el-form-item label="付款方式">
<el-select v-model="form.fukuantiaojian" placeholder="请选择">
<el-option v-for="option in wz_payment_terms" :key="option.value"
:label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="结算方式">
<el-select v-model="newProcurementForm.settlementMethod" placeholder="请选择">
<el-option label="请选择" value="" />
<!-- 可以添加更多选项 -->
<el-form-item label="发票开具方式">
<el-select v-model="form.fapiaoKjfs" placeholder="请选择">
<el-option v-for="option in wz_invoicing_way" :key="option.value"
:label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
@ -276,26 +271,17 @@
<!-- 附件上传 -->
<div class="form-section">
<h3>附件上传</h3>
<div class="upload-section">
<el-upload class="upload-demo" action="" :on-preview="handlePreview" :on-remove="handleRemove"
:before-remove="beforeRemove" multiple :limit="5" :on-exceed="handleExceed"
:file-list="newProcurementForm.fileList" list-type="text">
<el-button type="primary" :icon="Upload">上传文件</el-button>
<template #tip>
<div class="el-upload__tip">
请将文件拖到此处或点击上传<br>
最多上传5个文件单个文件大小不超过20M
</div>
</template>
</el-upload>
</div>
<file-upload ref="fileUploadRef" :isDrag="true" :file-list="form.opsCaigouPlanFilesBos"
:is-show-tip="false"
@update:file-list="handleUpdateFileList"
:file-type="['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf', 'png', 'jpg', 'jpeg']" />
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelNewProcurement">取消</el-button>
<el-button @click="saveDraft">保存草稿</el-button>
<el-button type="primary" @click="submitProcurement">提交申请</el-button>
<el-button @click="saveDraft" :loading="buttonLoading">保存草稿</el-button>
<el-button type="primary" @click="submitProcurement" :loading="buttonLoading">提交申请</el-button>
</div>
</template>
</el-dialog>
@ -374,196 +360,459 @@
color: #fff;
}
</style>
<script setup>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { Upload } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useProcurementDraftStore } from '@/store/modules/procurementDraft';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wz_invoicing_way, wz_payment_terms, wz_purchase_type, wz_contract_type, wz_caigou_examine } = toRefs<any>(proxy?.useDict('wz_invoicing_way', 'wz_payment_terms', 'wz_purchase_type', 'wz_contract_type', 'wz_caigou_examine'));
import { listCaigouPlan, getSupplierList, addCaigouPlan } from '@/api/wuziguanli/caigouPlan';
import { CaigouPlanVO, CaigouPlanQuery, CaigouPlanForm } from '@/api/wuziguanli/caigouPlan/types';
import { useRouter } from 'vue-router';
const router = useRouter();
// 导入用户store
import { useUserStore } from '@/store/modules/user';
// 获取用户store
const userStore = useUserStore();
const caigouPlanList = ref<CaigouPlanVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const total = ref(0);
const initFormData: CaigouPlanForm = {
id: undefined,
projectId: undefined,
jihuaName: undefined,
jihuaBianhao: undefined,
caigouDanwei: undefined,
caigouDanweiName: undefined,
jingbanren: undefined,
jingbanrenName: undefined,
hetonType: undefined,
caigouType: undefined,
cangkuUrl: undefined,
hetonName: undefined,
gonyingshangId: 1,
chuhuoTime: undefined,
fukuantiaojian: undefined,
fapiaoKjfs: undefined,
status: undefined,
shenheStatus: undefined,
yujiJine: undefined,
shijiJine: undefined,
opsCaigouPlanFilesBos: [],
opsCaigouPlanChanpinBos: [
{
chanpinName: '',
chanpinType: '',
chanpinMonovalent: 0,
goumaiNumber: 0,
danwei: '',
totalPrice: 0
}
],
}
const data = reactive<PageData<CaigouPlanForm, CaigouPlanQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
jihuaName: undefined,
jihuaBianhao: undefined,
caigouDanwei: undefined,
caigouDanweiName: undefined,
jingbanren: undefined,
jingbanrenName: undefined,
hetonType: undefined,
caigouType: undefined,
cangkuUrl: undefined,
hetonName: undefined,
gonyingshangId: 1,
chuhuoTime: undefined,
fukuantiaojian: undefined,
fapiaoKjfs: undefined,
status: undefined,
shenheStatus: undefined,
yujiJine: undefined,
shijiJine: undefined,
opsCaigouPlanChanpinBos: [
{
chanpinName: '',
chanpinType: '',
chanpinMonovalent: 0,
goumaiNumber: 0,
danwei: '',
totalPrice: 0
}
],
opsCaigouPlanFilesBos: undefined,
params: {
}
},
rules: {}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询运维-物资-采购计划单列表 */
const getList = async () => {
loading.value = true;
const res = await listCaigouPlan(queryParams.value);
caigouPlanList.value = res.rows;
total.value = res.total;
loading.value = false;
}
// 新增采购计划单
const addCaigouPlans = async () => {
buttonLoading.value = true; // 显示按钮加载状态
try {
// 提交表单数据到后端
const res = await addCaigouPlan(form.value);
if (res.code === 200) {
ElMessage({ message: '采购申请单已成功提交,等待审批!', type: 'success' });
// 刷新列表数据
getList();
// 关闭对话框并重置表单
resetNewProcurementForm();
isNewProcurementDialogVisible.value = false;
} else {
// 显示详细的错误信息
ElMessage({
message: res.msg || '新增采购计划单失败,请重试',
type: 'error'
});
}
} catch (error) {
ElMessage({ message: '失败', type: 'error' });
} finally {
buttonLoading.value = false; // 无论成功失败,都关闭加载状态
}
}
// 采购商列表
const supplierList = ref([]);
const getSupplierLists = async () => {
const res = await getSupplierList({
projectId: userStore.selectedProject.id
});
supplierList.value = res.rows;
}
onMounted(() => {
getList();
getSupplierLists();
});
// 监听用户选择的项目变化
watch(() => userStore.selectedProject, (newProject) => {
if (newProject && newProject.id) {
queryParams.value.projectId = newProject.id;
// 只在新增表单时设置projectId编辑表单保留原有值
if (!form.value.id) {
form.value.projectId = newProject.id;
}
// 调用getList刷新数据
getList();
}
}, { immediate: true, deep: true });
// 新建采购申请单对话框是否可见
const isNewProcurementDialogVisible = ref(false);
// 当前激活的标签页
const activeTab = ref('pending');
// 新建采购申请单表单数据
const newProcurementForm = reactive({
paymentTerms: '',
settlementMethod: '',
fileList: []
});
// 表格数据
const tableData = ref([
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
},
{
planNumber: 'PLAN-2023-0615-003',
planName: 'Q2风电轴承采购计划',
equipmentType: '风电设备',
requestDept: '运维部',
applicant: '王主管',
requestDate: '2023-06-15 10:30',
estimatedAmount: '300,000.00',
status: '待审批'
}
]);
// 分页相关
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(12);
// 切换标签页
const changeTab = (tab) => {
activeTab.value = tab;
// 这里可以根据标签页筛选数据
currentPage.value = 1; // 切换标签页时重置到第一页
};
// 获取状态标签类型
const getStatusTagType = (status) => {
switch (status) {
case '待审批':
return 'warning';
case '采购中':
return 'info';
case '未通过':
return 'danger';
case '已通过':
return 'primary';
case '已完成':
return 'success';
default:
return '';
}
};
// 查看详情
// 跳转查看详情
const handleView = (row) => {
console.log('查看采购计划详情:', row);
router.push({
path: '/materialManagement/planDetails',
query: {
planNumber: row.planNumber
id: row.id
}
});
// 这里可以实现查看详情的逻辑,比如打开详情弹窗或跳转到详情页
};
// 计算产品总价
const calculateTotalPrice = (row) => {
if (!row.chanpinMonovalent || !row.goumaiNumber) {
row.totalPrice = '0.00'; // 保存计算结果到对象中
return '0.00';
}
const price = parseFloat(row.chanpinMonovalent);
const quantity = parseInt(row.goumaiNumber);
if (isNaN(price) || isNaN(quantity)) {
row.totalPrice = '0.00'; // 保存计算结果到对象中
return '0.00';
}
const result = (price * quantity).toFixed(2);
row.totalPrice = result; // 保存计算结果到对象中
return result;
};
// 分页大小变化
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1;
// 添加产品
const addProduct = () => {
form.value.opsCaigouPlanChanpinBos.push({
chanpinName: '',
chanpinType: '',
chanpinMonovalent: 0,
goumaiNumber: 0,
danwei: '',
totalPrice: 0
});
};
// 当前页码变化
const handleCurrentChange = (current) => {
currentPage.value = current;
// 删除产品
const removeProduct = (index) => {
if (form.value.opsCaigouPlanChanpinBos.length <= 1) {
ElMessage({ message: '至少保留一个产品信息', type: 'warning' });
return;
}
form.value.opsCaigouPlanChanpinBos.splice(index, 1);
};
// 重置新建采购申请表单
const resetNewProcurementForm = () => {
form.value.jihuaName = '';
form.value.hetonName = '';
form.value.hetonType = '';
form.value.caigouType = '';
form.value.cangkuUrl = '';
form.value.danwei = '';
form.value.chuhuoTime = '';
form.value.fukuantiaojian = '';
form.value.fapiaoKjfs = '';
form.value.opsCaigouPlanChanpinBos = [{
chanpinName: '',
chanpinType: '',
chanpinMonovalent: '',
goumaiNumber: '',
danwei: '',
totalPrice: ''
}];
form.value.opsCaigouPlanFilesBos = [];
};
// 取消新建采购申请
const cancelNewProcurement = () => {
// 检查是否有未保存的内容
const hasContent = Object.values(form.value).some(value => {
if (Array.isArray(value)) {
return value.length > 0 &&
value.some(item =>
typeof item === 'object' &&
Object.values(item).some(v => v)
);
}
return !!value;
});
if (hasContent) {
ElMessageBox.confirm('表单内容尚未保存,确定要关闭吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
resetNewProcurementForm();
isNewProcurementDialogVisible.value = false;
});
} else {
resetNewProcurementForm();
isNewProcurementDialogVisible.value = false;
}
};
// 草稿校验函数
const validateDraft = () => {
// 草稿只需要计划名称作为必填项
if (!form.value.jihuaName.trim()) {
ElMessage({ message: '请填写计划名称', type: 'error' });
return false;
}
// 检查已填写的产品信息的有效性
for (let i = 0; i < form.value.opsCaigouPlanChanpinBos.length; i++) {
const product = form.value.opsCaigouPlanChanpinBos[i];
if (product.chanpinName && !product.chanpinType) {
ElMessage({ message: `${i + 1}行产品:填写了产品名称,请也填写产品型号`, type: 'warning' });
}
if (product.productPrice && parseFloat(product.productPrice) <= 0) {
ElMessage({ message: `${i + 1}行产品产品单价应大于0`, type: 'warning' });
}
if (product.purchaseQuantity && parseInt(product.purchaseQuantity) <= 0) {
ElMessage({ message: `${i + 1}行产品购买数量应大于0`, type: 'warning' });
}
}
return true;
};
// 保存草稿
const saveDraft = async () => {
// 验证草稿
if (!validateDraft()) {
return;
}
buttonLoading.value = true; // 显示按钮加载状态
try {
// 使用pinia store保存草稿
const draftStore = useProcurementDraftStore();
const savedDraft = draftStore.saveDraft(form.value.jihuaName, form.value);
console.log('保存草稿:', {
draftNumber: savedDraft.draftNumber,
saveTime: savedDraft.saveTime,
content: savedDraft.content
});
ElMessage({ message: `草稿已成功保存(编号:${savedDraft.draftNumber}),您可以在草稿箱中查看`, type: 'success' });
} catch (error) {
console.error('保存草稿失败:', error);
ElMessage({
message: '保存草稿失败,请重试',
type: 'error'
});
} finally {
buttonLoading.value = false; // 无论成功失败,都关闭加载状态
}
};
// 表单校验函数
const validateForm = () => {
// 基础信息校验
if (!form.value.jihuaName.trim()) {
ElMessage({ message: '请填写计划名称', type: 'error' });
return false;
}
if (!form.value.hetonName.trim()) {
ElMessage({ message: '请填写合同名称', type: 'error' });
return false;
}
if (!form.value.hetonType) {
ElMessage({ message: '请选择合同类型', type: 'error' });
return false;
}
if (!form.value.caigouType) {
ElMessage({ message: '请选择采购类型', type: 'error' });
return false;
}
if (!form.value.cangkuUrl) {
ElMessage({ message: '请选择仓库地址', type: 'error' });
return false;
}
if (!form.value.danwei) {
ElMessage({ message: '请选择供应商单位', type: 'error' });
return false;
}
// 产品信息校验
const hasValidProduct = form.value.opsCaigouPlanChanpinBos.some(product => {
return product.chanpinName &&
product.chanpinType &&
product.chanpinMonovalent && parseFloat(product.chanpinMonovalent) > 0 &&
product.goumaiNumber && parseInt(product.goumaiNumber) > 0 &&
product.danwei;
});
if (!hasValidProduct) {
ElMessage({ message: '请至少填写一个有效的产品信息', type: 'error' });
return false;
}
// 检查每个产品的有效性
for (let i = 0; i < form.value.opsCaigouPlanChanpinBos.length; i++) {
const product = form.value.opsCaigouPlanChanpinBos[i];
if (product.chanpinName || product.chanpinType || product.chanpinMonovalent || product.goumaiNumber) {
if (!product.chanpinName) {
ElMessage({ message: `${i + 1}行产品:请填写产品名称`, type: 'error' });
return false;
}
if (!product.chanpinType) {
ElMessage({ message: `${i + 1}行产品:请填写产品型号`, type: 'error' });
return false;
}
if (!product.chanpinMonovalent) {
ElMessage({ message: `${i + 1}行产品:请填写产品单价`, type: 'error' });
return false;
}
if (parseFloat(product.chanpinMonovalent) <= 0) {
ElMessage({ message: `${i + 1}行产品产品单价必须大于0`, type: 'error' });
return false;
}
if (!product.goumaiNumber) {
ElMessage({ message: `${i + 1}行产品:请填写购买数量`, type: 'error' });
return false;
}
if (parseInt(product.goumaiNumber) <= 0) {
ElMessage({ message: `${i + 1}行产品购买数量必须大于0`, type: 'error' });
return false;
}
if (!product.danwei) {
ElMessage({ message: `${i + 1}行产品:请填写单位`, type: 'error' });
return false;
}
}
}
return true;
};
// 提交申请
const submitProcurement = async () => {
// 在提交前,为所有产品行重新计算并保存总价
form.value.opsCaigouPlanChanpinBos.forEach(product => {
calculateTotalPrice(product);
});
// 表单验证
if (!validateForm()) {
return;
}
try {
// 确认提交
await ElMessageBox.confirm(
'确定要提交采购申请单吗?提交后将进入审批流程,不可撤销。',
'确认提交',
{
confirmButtonText: '确认提交',
cancelButtonText: '取消',
type: 'warning'
}
);
// 调用提交函数
await addCaigouPlans();
} catch (error) {
// 处理用户取消或其他错误
if (error !== 'cancel') {
console.error('提交采购申请单时发生错误:', error);
ElMessage({ message: '提交过程中发生错误,请重试', type: 'error' });
}
}
}
// });
// 处理文件上传完成后获取完整文件列表
const handleUpdateFileList = (fileList) => {
form.value.opsCaigouPlanFilesBos = fileList.map(file => ({
fileId: file.ossId,
fileName: file.name,
fileUrl: file.url,
}));
};
</script>

View File

@ -156,65 +156,145 @@
</div>
</div>
<div style="margin-top: 30px;">
<div class="menu" style="background-color: #F2F2F2; padding: 20px;">
<el-row gutter="30">
<el-col :span="3">
<el-input placeholder="请输入备件名称"></el-input>
</el-col>
<el-col :span="3">
<el-select placeholder="设备类型">
</el-select>
</el-col>
<el-col :span="3">
<el-select placeholder="备件类别">
</el-select>
</el-col>
<el-col :span="3">
<el-select placeholder="全部状态">
</el-select>
</el-col>
<el-col :span="8">
<el-button icon="search" type="primary">搜索</el-button>
<el-button icon="refresh">重置</el-button>
</el-col>
</el-row>
</div>
<el-table :data="pagedTableData" border style="width: 100%;margin-top: 10px;">
<el-table-column prop="backupNumber" label="备件编号" />
<el-table-column prop="backupName" label="备件名称" />
<el-table-column prop="equipmentType" label="设备类型" />
<el-table-column prop="specificationModel" label="规格型号" />
<el-table-column prop="inventoryStatus" label="库存状态" />
<el-table-column prop="inventoryQuantity" label="库存数量" />
<el-table-column label="安全库存">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<!-- 第一排输入框 -->
<div style="width: 100%; margin-bottom: 10px;">
<el-form-item label="备件编号" prop="beijianNumber" style="margin-right: 20px;">
<el-input v-model="queryParams.beijianNumber" placeholder="请输入备件编号" clearable
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="备件名称" prop="beijianName" style="margin-right: 20px;">
<el-input v-model="queryParams.beijianName" placeholder="请输入备件名称" clearable
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="规格型号" prop="guigexinghao">
<el-input v-model="queryParams.guigexinghao" placeholder="请输入规格型号" clearable
@keyup.enter="handleQuery" />
</el-form-item>
</div>
<!-- 第二排下拉框和按钮 -->
<div style="width: 100%;">
<el-form-item label="设备类型" prop="shebeiType" style="margin-right: 20px;">
<el-select v-model="queryParams.shebeiType" placeholder="请选择设备类型" clearable>
<el-option v-for="dict in wz_device_type" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="库存状态" prop="kucunStatus" style="margin-right: 20px;">
<el-select v-model="queryParams.kucunStatus" placeholder="请选择库存状态" clearable>
<el-option v-for="dict in wz_inventory_type" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</div>
</el-form>
</el-card>
</div>
</transition>
<el-table v-loading="loading" border :data="beipinBeijianList" style="width: 100%;margin-top: 10px;">
<el-table-column label="备件编号" align="center" prop="beijianNumber" />
<el-table-column label="备件名称" align="center" prop="beijianName" />
<el-table-column label="设备类型" align="center" prop="shebeiType">
<template #default="scope">
<el-tag :type="getTagType(scope.row.safetyStockStatus)">
{{ scope.row.safetyStockStatus }}
</el-tag>
{{ getDictLabel(wz_device_type, scope.row.shebeiType) }}
</template>
</el-table-column>
<el-table-column label="操作">
<el-table-column label="规格型号" align="center" prop="guigexinghao" />
<el-table-column label="库存数量" align="center" prop="kucunCount" />
<el-table-column label="库存状态" align="center" prop="kucunStatus">
<template #default="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
<dict-tag :options="wz_inventory_type" :value="scope.row.kucunStatus"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button type="text" @click="handleUpdate(scope.row)"
v-hasPermi="['personnel:beipinBeijian:edit']">编辑</el-button>
<el-button type="text" @click="handleDetail(scope.row)"
v-hasPermi="['personnel:beipinBeijian:query']">详情</el-button>
<el-button type="text" @click="handleDelete(scope.row)"
v-hasPermi="['personnel:beipinBeijian:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-section">
<div class="pagination-info">
显示第{{ (currentPage - 1) * pageSize + 1 }}{{ Math.min(currentPage * pageSize, total) }}共有{{
显示第{{ (data.queryParams.pageNum - 1) * data.queryParams.pageSize + 1 }}{{ Math.min(data.queryParams.pageNum * data.queryParams.pageSize, total) }}共有{{
total }}条记录
</div>
<div class="pagination-controls">
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="currentPage" :page-sizes="[10, 20, 30, 40]" :page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="total" background>
</el-pagination>
<pagination v-show="total > 0" :total="total" v-model:page="data.queryParams.pageNum"
v-model:limit="data.queryParams.pageSize" @pagination="getList" />
</div>
</div>
</div>
</el-card>
<!-- 编辑弹窗 -->
<el-dialog title="编辑备件信息" v-model="dialog.visible" width="50%" append-to-body>
<el-form ref="beipinBeijianFormRef" :model="form" :rules="rules" label-width="120px"
style="max-width: 600px;">
<el-form-item label="备件编号" prop="beijianNumber">
<el-input v-model="form.beijianNumber" placeholder="请输入备件编号" />
</el-form-item>
<el-form-item label="备件名称" prop="beijianName">
<el-input v-model="form.beijianName" placeholder="请输入备件名称" />
</el-form-item>
<el-form-item label="规格型号" prop="guigexinghao">
<el-input v-model="form.guigexinghao" placeholder="请输入规格型号" />
</el-form-item>
<el-form-item label="库存数量" prop="kucunCount">
<el-input v-model="form.kucunCount" placeholder="请输入库存数量" />
</el-form-item>
<el-form-item label="库存状态" prop="kucunStatus">
<el-select v-model="form.kucunStatus" placeholder="请选择库存状态">
<el-option v-for="dict in wz_inventory_type" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备类型" prop="shebeiType">
<el-select v-model="form.shebeiType" placeholder="请选择设备类型">
<el-option v-for="dict in wz_device_type" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog title="备件详情" v-model="detailDialogVisible" width="50%" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="备件编号">{{ detailData.beijianNumber }}</el-descriptions-item>
<el-descriptions-item label="备件名称">{{ detailData.beijianName }}</el-descriptions-item>
<el-descriptions-item label="规格型号">{{ detailData.guigexinghao }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ getDictLabel(wz_device_type, detailData.shebeiType)
}}</el-descriptions-item>
<el-descriptions-item label="库存数量">{{ detailData.kucunCount }}</el-descriptions-item>
<el-descriptions-item label="库存状态">
<dict-tag :options="wz_inventory_type" :value="detailData.kucunStatus"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDetailDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
@ -284,156 +364,291 @@
background-color: #409eff;
color: #fff;
}
/* 详情弹窗样式 */
.detail-container {
padding: 20px 0;
}
.detail-item {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.detail-label {
width: 120px;
font-weight: 500;
color: #303133;
margin-right: 20px;
}
.detail-value {
flex: 1;
color: #606266;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
padding-top: 20px;
}
</style>
<script setup>
import TitleComponent from '@/components/TitleComponent';
<script setup lang="ts">
import { ref, computed } from 'vue';
import TitleComponent from '@/components/TitleComponent/index.vue';
// 计算属性:根据当前页码和每页条数获取分页后的数据
const tableData = ref([
{
backupNumber: 'SOL-2023-001',
backupName: '光伏逆变器模块',
equipmentType: '光伏设备',
specificationModel: 'SGGKTL-M',
inventoryStatus: '12个',
inventoryQuantity: '5个',
safetyStockStatus: '正常'
},
{
backupNumber: 'SOL-2023-001',
backupName: '光伏逆变器模块',
equipmentType: '光伏设备',
specificationModel: 'SGGKTL-M',
inventoryStatus: '12个',
inventoryQuantity: '5个',
safetyStockStatus: '正常'
},
{
backupNumber: 'SOL-2023-001',
backupName: '光伏逆变器模块',
equipmentType: '光伏设备',
specificationModel: 'SGGKTL-M',
inventoryStatus: '12个',
inventoryQuantity: '5个',
safetyStockStatus: '正常'
},
{
backupNumber: 'SOL-2023-001',
backupName: '光伏逆变器模块',
equipmentType: '光伏设备',
specificationModel: 'SGGKTL-M',
inventoryStatus: '12个',
inventoryQuantity: '5个',
safetyStockStatus: '正常'
},
{
backupNumber: 'WIN-2023-045',
backupName: '风力发电机轴承',
equipmentType: '风电设备',
specificationModel: '6318-2RS1/C3',
inventoryStatus: '3套',
inventoryQuantity: '5套',
safetyStockStatus: '低库存'
},
{
backupNumber: 'SOL-2023-001',
backupName: '光伏逆变器模块',
equipmentType: '光伏设备',
specificationModel: 'SGGKTL-M',
inventoryStatus: '12个',
inventoryQuantity: '5个',
safetyStockStatus: '正常'
},
{
backupNumber: 'WIN-2023-045',
backupName: '风力发电机轴承',
equipmentType: '风电设备',
specificationModel: '6318-2RS1/C3',
inventoryStatus: '3套',
inventoryQuantity: '5套',
safetyStockStatus: '低库存'
},
{
backupNumber: 'WIN-2023-045',
backupName: '风力发电机轴承',
equipmentType: '风电设备',
specificationModel: '6318-2RS1/C3',
inventoryStatus: '0套',
inventoryQuantity: '2套',
safetyStockStatus: '缺货'
},
{
backupNumber: 'WIN-2023-045',
backupName: '风力发电机轴承',
equipmentType: '风电设备',
specificationModel: '6318-2RS1/C3',
inventoryStatus: '3套',
inventoryQuantity: '5套',
safetyStockStatus: '低库存'
},
{
backupNumber: 'WIN-2023-045',
backupName: '风力发电机轴承',
equipmentType: '风电设备',
specificationModel: '6318-2RS1/C3',
inventoryStatus: '3套',
inventoryQuantity: '5套',
safetyStockStatus: '低库存'
},
{
backupNumber: 'WIN-2023-045',
backupName: '风力发电机轴承',
equipmentType: '风电设备',
specificationModel: '6318-2RS1/C3',
inventoryStatus: '3套',
inventoryQuantity: '5套',
safetyStockStatus: '低库存'
}
])
// 当前页码
const currentPage = ref(1);
// 每页条数 - 与分页控件默认值保持一致
const pageSize = ref(10);
// 总条数 - 从原始数据计算得出
const total = ref(tableData.value.length);
const pagedTableData = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value;
const endIndex = startIndex + pageSize.value;
return tableData.value.slice(startIndex, endIndex);
// 导入用户store
import { useUserStore } from '@/store/modules/user';
// 获取用户store
const userStore = useUserStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { listBeipinBeijian, getBeipinBeijian, delBeipinBeijian, updateBeipinBeijian } from '@/api/wuziguanli/beijian';
import { BeipinBeijianVO, BeipinBeijianQuery, BeipinBeijianForm } from '@/api/wuziguanli/beijian/types';
const { wz_inventory_type, wz_device_type } = toRefs<any>(proxy?.useDict('wz_inventory_type', 'wz_spareparts_type', 'wz_device_type'));
const beipinBeijianList = ref<BeipinBeijianVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const total = ref(0);
// 详情相关数据
const detailData = ref<BeipinBeijianVO>({
id: undefined,
projectId: undefined,
beijianNumber: undefined,
beijianName: undefined,
shebeiType: undefined,
guigexinghao: undefined,
kucunStatus: undefined,
kucunCount: undefined,
});
// 当前页码改变
const handleCurrentChange = (val) => {
currentPage.value = val;
const detailDialogVisible = ref(false);
const queryFormRef = ref<ElFormInstance>();
const beipinBeijianFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: BeipinBeijianForm = {
id: undefined,
projectId: undefined,
beijianNumber: undefined,
beijianName: undefined,
shebeiType: undefined,
guigexinghao: undefined,
kucunStatus: undefined,
kucunCount: undefined,
}
const data = reactive<PageData<BeipinBeijianForm, BeipinBeijianQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
beijianNumber: undefined,
beijianName: undefined,
shebeiType: undefined,
guigexinghao: undefined,
kucunStatus: undefined,
kucunCount: undefined,
params: {
}
},
rules: {
beijianName: [
{ required: true, message: "备件名称不能为空", trigger: "blur" }
],
shebeiType: [
{ required: true, message: "设备类型不能为空", trigger: "change" }
],
guigexinghao: [
{ required: true, message: "规格型号不能为空", trigger: "blur" }
],
kucunStatus: [
{ required: true, message: "库存状态不能为空", trigger: "change" }
],
kucunCount: [
{ required: true, message: "库存数量不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
// 根据字典值获取标签信息的辅助函数
const getDictLabel = (dictType, value) => {
// 健壮性检查
if (!value || !dictType || !Array.isArray(dictType)) {
return value;
}
// 使用find方法更高效地查找匹配项
const option = dictType.find(item => item?.value === value);
// 如果找到匹配项,返回标签,否则返回原始值
return option?.label || value;
};
// 根据安全库存状态获取标签类型
const getTagType = (status) => {
if (status === '正常') {
return 'success'
} else if (status === '低库存') {
return 'warning'
} else if (status === '缺货') {
return 'danger'
/** 查询运维-物资-备品配件列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listBeipinBeijian(queryParams.value);
beipinBeijianList.value = res.rows;
total.value = res.total;
} catch (error) {
proxy?.$modal.msgError('获取数据失败,请重试');
console.error('获取备品配件列表失败:', error);
} finally {
loading.value = false;
}
return ''
}
// 编辑操作方法
const handleEdit = (row) => {
console.log('编辑', row)
// 这里可以编写编辑的逻辑,比如跳转到编辑页面等
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
// 详情操作方法
const handleDetail = (row) => {
console.log('详情', row)
// 这里可以编写查看详情的逻辑
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
beipinBeijianFormRef.value?.resetFields();
}
// 删除操作方法
const handleDelete = (row) => {
console.log('删除', row)
// 这里可以编写删除的逻辑,比如提示确认删除,然后从表格数据中移除等
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 修改按钮操作 */
const handleUpdate = async (row?: BeipinBeijianVO) => {
reset();
const _id = row?.id || ids.value[0];
if (!_id) {
proxy?.$modal.msgWarning('请选择需要编辑的数据');
return;
}
try {
const res = await getBeipinBeijian(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
} catch (error) {
proxy?.$modal.msgError('获取数据失败,请重试');
console.error('获取备品配件详情失败:', error);
}
}
/** 详情按钮操作 */
const handleDetail = async (row?: BeipinBeijianVO) => {
const _id = row?.id || ids.value[0];
if (!_id) {
proxy?.$modal.msgWarning('请选择需要查看的数据');
return;
}
try {
const res = await getBeipinBeijian(_id);
detailData.value = res.data;
detailDialogVisible.value = true;
} catch (error) {
proxy?.$modal.msgError('获取数据失败,请重试');
console.error('获取备品配件详情失败:', error);
}
}
/** 关闭详情弹窗 */
const closeDetailDialog = () => {
detailDialogVisible.value = false;
}
/** 提交按钮 */
const submitForm = () => {
beipinBeijianFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateBeipinBeijian(form.value);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
} catch (error) {
proxy?.$modal.msgError('操作失败,请重试');
console.error('提交表单失败:', error);
} finally {
buttonLoading.value = false;
}
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: BeipinBeijianVO) => {
const _ids = row?.id || ids.value;
if (!_ids || (_ids instanceof Array && _ids.length === 0)) {
proxy?.$modal.msgWarning('请选择需要删除的数据');
return;
}
try {
await proxy?.$modal.confirm('是否确认删除运维-物资-备品配件编号为"' + _ids + '"的数据项?');
loading.value = true;
await delBeipinBeijian(_ids);
proxy?.$modal.msgSuccess("删除成功");
await getList();
} catch (error) {
// 如果是用户取消确认,则不显示错误信息
if (error !== 'cancel') {
proxy?.$modal.msgError('删除失败,请重试');
console.error('删除数据失败:', error);
}
} finally {
loading.value = false;
}
}
// 监听用户选择的项目变化
watch(() => userStore.selectedProject, (newProject) => {
if (newProject && newProject.id) {
queryParams.value.projectId = newProject.id;
// 只在新增表单时设置projectId编辑表单保留原有值
if (!form.value.id) {
form.value.projectId = newProject.id;
}
// 调用getList刷新数据
getList();
}
}, { immediate: true, deep: true });
onMounted(() => {
getList();
});
// 组件卸载时清空projectId
onUnmounted(() => {
queryParams.value.projectId = undefined;
form.value.projectId = undefined;
});
</script>

View File

@ -9,64 +9,17 @@
<span class="update-time">截止至2025/06/30 12:00</span>
</el-col>
</el-row>
<!-- 关键指标卡片区域 -->
<el-row class="metrics-container" :gutter="0">
<el-col :span="6">
<el-col v-for="card in cardData" :key="card.key" :span="6">
<div class="metric-card">
<div class="metric-value">{{ props.dashboardData.todayAlarmTotal }}</div>
<div class="metric-label">今日报警总数</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates.todayAlarmTotal.type">
<img v-if="props.dashboardData.updates.todayAlarmTotal.type === 'up'" src="/src/assets/demo/up.png"
<div class="metric-value">{{ props.dashboardData[card.key] }}</div>
<div class="metric-label">{{ card.label }}</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates[card.updateKey].type">
<img v-if="props.dashboardData.updates[card.updateKey].type === 'up'" src="/src/assets/demo/up.png"
class="trend-icon" alt="上升">
<img v-else src="/src/assets/demo/down.png" class="trend-icon" alt="下降">{{
props.dashboardData.updates.todayAlarmTotal.value }}
</span>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="metric-card">
<div class="metric-value">{{ props.dashboardData.unhandledAlarms }}</div>
<div class="metric-label">未处理报警</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates.unhandledAlarms.type">
<img v-if="props.dashboardData.updates.unhandledAlarms.type === 'up'" src="/src/assets/demo/up.png"
class="trend-icon" alt="上升">
<img v-else src="/src/assets/demo/down.png" class="trend-icon" alt="下降">{{
props.dashboardData.updates.unhandledAlarms.value }}
</span></div>
</div>
</el-col>
<el-col :span="6">
<div class="metric-card">
<div class="metric-value">{{ props.dashboardData.handledAlarms }}</div>
<div class="metric-label">已处理报警</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates.handledAlarms.type">
<img v-if="props.dashboardData.updates.handledAlarms.type === 'up'" src="/src/assets/demo/up.png"
class="trend-icon" alt="上升">
<img v-else src="/src/assets/demo/down.png" class="trend-icon" alt="下降">{{
props.dashboardData.updates.handledAlarms.value }}
</span></div>
</div>
</el-col>
<el-col :span="6">
<div class="metric-card">
<div class="metric-value">{{ props.dashboardData.avgProcessTime }}</div>
<div class="metric-label">平均处理时长</div>
<div class="metric-change">
较上周
<span :class="props.dashboardData.updates.avgProcessTime.type">
<img v-if="props.dashboardData.updates.avgProcessTime.type === 'up'" src="/src/assets/demo/up.png"
class="trend-icon" alt="上升">
<img v-else src="/src/assets/demo/down.png" class="trend-icon" alt="下降">{{
props.dashboardData.updates.avgProcessTime.value }}
props.dashboardData.updates[card.updateKey].value }}
</span>
</div>
</div>
@ -176,6 +129,30 @@ const props = defineProps({
}
});
// 卡片数据配置
const cardData = [
{
key: 'todayAlarmTotal',
label: '今日报警总数',
updateKey: 'todayAlarmTotal'
},
{
key: 'unhandledAlarms',
label: '未处理报警',
updateKey: 'unhandledAlarms'
},
{
key: 'handledAlarms',
label: '已处理报警',
updateKey: 'handledAlarms'
},
{
key: 'avgProcessTime',
label: '平均处理时长',
updateKey: 'avgProcessTime'
}
];
const timeRange = ref('7days');
const alarmCountRef = ref(null);
const processEfficiencyRef = ref(null);

View File

@ -1,11 +1,13 @@
<template>
<div class="pie-chart-container">
<!-- 标题栏 -->
<div class="chart-header">
<TitleComponent title="报警类型分布" :fontLevel="2" />
<el-select v-model="selectedTimeRange" placeholder="选择时间范围" size="small">
<el-option label="今日" value="today" />
</el-select>
</div>
<!-- 图表 -->
<div ref="pieChartRef" class="chart-content"></div>
</div>
</template>
@ -27,6 +29,7 @@ const selectedTimeRange = ref('today');
const pieChartRef = ref(null);
let chartInstance = null;
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);

View File

@ -2,7 +2,7 @@
<div style="padding: 20px;">
<el-row>
<el-col :span="15">
<el-row>
<el-row style="margin: 20px 0;">
<TitleComponent title="设备情况" subtitle="电站一次监控数据" />
<sbqk />
</el-row>

View File

@ -0,0 +1,95 @@
<template>
<el-dialog :visible="dialogVisible" width="1200" id="custom-dialog" class="no-header-dialog normal">
<div class="back">
<div class="alarm-alert-content success">
<el-row>
<el-col :span="20">
<div class="top">
<div class="info">
<div class="title">通信中断</div>
<div class="alarm-id">告警IDINV-2023-003</div>
<div class="status-box">
<div class="status red">状态待处理</div>
<div class="last-update">最后更新刚刚</div>
</div>
</div>
<div class="info-box">
<div class="list">
<div class="item">
<div>
<div class="title">告警位置</div>
<div class="text">光伏区A区-3-08</div>
</div>
<div>
<div class="title">告警级别</div>
<div class="text">二级告警紧急</div>
</div>
</div>
<div class="item">
<div>
<div class="title">预计解决时间</div>
<div class="text">2025-09-19 18:00</div>
</div>
<div>
<div class="title">上报时间</div>
<div class="text">2025-09-18 18:00</div>
</div>
</div>
<div class="item">
<div>
<div class="title">设备负责人</div>
<div class="text">李华现场运维组</div>
</div>
<div>
<div class="title">通知方式</div>
<div class="text">系统消息短信电话</div>
</div>
</div>
</div>
</div>
</div>
<el-divider vertical border-style="dashed"></el-divider>
<!-- 进度 -->
<div class="progress-box" style="margin-bottom: 24px;">
<div class="title">处理进度</div>
<el-progress :text-inside="true" color="#ABABAB" :stroke-width="10" :percentage="3"
:show-text="false" />
</div>
<div class="notice-box">
<div class="item">
<div class="title active">告警产生并通知</div>
<div class="time">2025-09-19 18:05</div>
<div class="content">系统检测到告警并通知负责人</div>
</div>
<div class="item">
<div class="title">任务指派</div>
<div class="time"></div>
<div class="content">指派任务给相关责任人</div>
</div>
<div class="item">
<div class="title">开始处理</div>
<div class="time"></div>
<div class="content">运维人员开始处理告警</div>
</div>
<div class="item">
<div class="title">告警产生</div>
<div class="time"></div>
<div class="content">告警已解决并记录结果</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</el-dialog>
</template>
<script setup>
const props = defineProps({
dialogVisible: {
type: Boolean,
required: true, // 若外部必传设为true否则可加default: false
default: false // 可选:若外部可能不传,设置默认值
}
});
</script>

File diff suppressed because one or more lines are too long

View File

@ -2,13 +2,16 @@
<div class="cardItem">
<el-card>
<div class="tianqi"
style="display: flex; flex-direction: column; align-items: center; background-color: #fafafa; border-radius: 10px; padding-bottom: 40px">
<div>
<img src="/assets/Sunny.png" style="display: block; width: 100px; height: 100px" alt="" />
style="display: flex; flex-direction: column; align-items: center; background-color: #fafafa; border-radius: 10px; padding-bottom: 40px;padding: 20px;">
<div
style="width: 100%;display: flex;flex-direction: column;align-items: center;background-color: #FFFFFF;margin: 0 20px;border-radius: 15px;padding: 20px;">
<div>
<img src="/assets/Sunny.png" style="display: block; width: 100px; height: 100px" alt="" />
</div>
<div style="font-family: 'Alibaba-PuHuiTi-Bold'; font-size: 24px">31</div>
<div>晴朗</div>
<div style="color: rgba(154, 154, 154, 1); font-size: 14px">紫外线强度<span></span></div>
</div>
<div style="font-family: 'Alibaba-PuHuiTi-Bold'; font-size: 24px">31</div>
<div>晴朗</div>
<div style="color: rgba(154, 154, 154, 1); font-size: 14px">紫外线强度<span></span></div>
<div class="tianqi2">
<div class="item">
<div>

View File

@ -1,7 +1,8 @@
<template>
<el-card shadow="never" style="border-radius: 10px;">
<el-form :inline="true" :model="formInline" label-width="120" style="display: flex; justify-content: center;">
<el-form ref="formRef" :inline="true" :model="formInline" label-width="120"
style="display: flex; justify-content: center;">
<el-form-item label="规则编号">
<el-input v-model="formInline.user" placeholder="请输入规则编号" clearable />
</el-form-item>
@ -43,7 +44,7 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template #default="scope">
<el-button link type="primary">详情</el-button>
<el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
<el-button link type="danger">处理</el-button>
<el-button link type="warning">维护记录</el-button>
</template>
@ -54,37 +55,24 @@
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</el-card>
<el-dialog v-model="dialogVisible" width="1000" id="custom-dialog" class="no-header-dialog normal">
<div class="alert-content">
<div class="img">
<img src="/assets/dialog1.png" alt="">
</div>
</div>
</el-dialog>
<CustomDialogStatus v-model="CustomDialogStatusVisible" v-model:dialogVisible="dialogVisible" :height="dialogHeight"
close-on-click-modal />
<CustomDialogAlarm ref=" dialogAlarmRef" v-model="CustomDialogAlarmVisible" :height="dialogHeight"
@opened="adjustDialogHeight" close-on-click-modal />
</template>
<style lang="scss" scoped></style>
<style lang="scss">
// #custom-dialog {
// padding: 0;
// .el-dialog__header {
// display: none;
// }
// .el-dialog__body {
// padding: 0 !important;
// }
// .alert-content {
// padding: 80px;
// background: linear-gradient(180deg, rgba(0, 119, 255, 0.23) 0%, rgba(255, 255, 255, 0) 100%);
// }
// }</style>
<script setup>
import CustomDialogStatus from './CustomDialogStatus.vue'
import CustomDialogAlarm from './CustomDialogAlarm.vue'
const formInline = ref({})
const total = ref(0);
const loading = ref(false);
const dialogVisible = ref(true);
const CustomDialogStatusVisible = ref(false);
const CustomDialogAlarmVisible = ref(false);
const listData = [
{ id: "INV-2023-001", shuchu: "12.8kw", xiaolv: "98.2%", wendu: "42℃", fadian: "158.5kWh", status: 1 },
{ id: "INV-2023-001", shuchu: "12.8kw", xiaolv: "98.2%", wendu: "42℃", fadian: "158.5kWh", status: 1 },
@ -110,6 +98,13 @@ const statusMap = {
const initFormData = {
};
const handleDetail = (row) => {
if (row.status === 1) {
CustomDialogStatusVisible.value = true;
} else {
CustomDialogAlarmVisible.value = true;
}
}
const data = reactive({
form: { ...initFormData },
queryParams: {

View File

@ -5,7 +5,8 @@
<TitleComponent title="XXX电站·逆变器综合监控" subtitle="实时监控X台逆变器运行状态、发电趋势及环境信息" />
</el-col>
<!-- 外层col控制整体宽度并右对齐同时作为flex容器 -->
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
<el-col :span="12"
style="display: flex; justify-content: flex-end; align-items: center;margin-bottom: 20px;">
<!-- 子col1输入框容器 -->
<el-col :span="8">
<el-input placeholder="请输入逆变器...">
@ -32,7 +33,7 @@
</el-col>
</el-col>
</el-row>
<el-row style="display: flex; gap: 30px;" class="card">
<el-row style="display: flex; gap: 30px;margin: 20px 0;" class="card">
<el-col style="flex: 1;" class="item">
<div class="box" style="height: 100%;display: flex;">
<div class="left"
@ -161,7 +162,7 @@
</div>
</el-col>
</el-row>
<el-row>
<el-row style="margin: 20px 0;">
<el-col :span="15">
<el-row style="display: flex;flex-direction: column;height: 100%;">
<div>
@ -177,7 +178,7 @@
<QiXiang />
</el-col>
</el-row>
<el-row :gutter="20"> <!-- gutter列之间的间距单位px控制垂直/水平间距 -->
<el-row style="margin: 20px 0;"> <!-- gutter列之间的间距单位px控制垂直/水平间距 -->
<!-- 标题列占满12列栅格系统默认12列 -->
<el-col> <!-- span=12 表示占满一行宽度 -->
<TitleComponent title="逆变器运行状态" :fontLevel="2" />

View File

@ -9,13 +9,13 @@
<el-col :span="12">
<div class="item">
<div class="status">在线</div>
<div class="count" style="color: rgba(0, 184, 122, 1);">56</div>
<div class="count" style="color: rgba(0, 184, 122, 1);">{{ data?.sumOnLine || 0 }}</div>
</div>
</el-col>
<el-col :span="12">
<div class="item">
<div class="status">离线</div>
<div class="count" style="color: rgba(102, 102, 102, 1);">10</div>
<div class="count" style="color: rgba(102, 102, 102, 1);">{{ data?.sumOffLine || 0 }}</div>
</div>
</el-col>
<el-col :span="12">
@ -146,4 +146,11 @@
}
}
</style>
<script setup></script>
<script setup>
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
</script>

View File

@ -1,7 +1,7 @@
<template>
<el-row style="height: 100%;">
<el-card style="width: 100%;border-radius: 12px;height: 100%;">
<div style="display: flex;width: 100%;justify-content: space-between;align-items: center;">
<div style="display: flex;width: 100%;justify-content: flex-end;align-items: center;padding-bottom: 15px;">
<TitleComponent title="实时视频监控" subtitle="查看所有已完成的巡检记录,跟进巡检报告" />
<span style="color: rgba(24, 109, 245, 1);display: flex;align-items: center; cursor: pointer;">
<span>
@ -14,36 +14,52 @@
</span>
</div>
<div class="video-container">
<el-row gutter="20">
<el-row :gutter="20">
<!-- 扩展布局左侧大视频 + 右侧小视频列 -->
<template v-if="isExpanded">
<!-- 左侧大视频 16 -->
<el-col :span="16">
<div class="item large" @click="() => { isExpanded = false; }">
<img :src="videoList[activeIndex].url" alt="">
<el-col :span="16" class="video-wrapper">
<div class="item large" @click="() => { isExpanded = false; }" ref="bigVideoRef"
:id="`bigVideo`">
<div class="title" v-if="isExpanded"
style="display: flex;justify-content: space-between;">
<div class="text">
<!-- <div class="text">
{{ videoList[activeIndex].name }}
</div>
<div class="tools">
<img src="/assets/svg/huanyuan.svg" alt=""></img>
<img src="/assets/svg/quanpin.svg" alt=""></img>
<img src="/assets/svg/jietu.svg" alt="">
</div>
</div> -->
</div>
<div class="title" v-else>{{ videoList[activeIndex].name }}</div>
</div>
<!-- 大视频的切换视图按钮 -->
<div class="video-action-btn">
<el-button type="primary" @click.stop="() => {
isExpanded = false;
}">切换视图</el-button>
</div>
</el-col>
<!-- 右侧小视频列 8 -->
<el-col :span="8">
<el-row gutter="20">
<el-col :span="24" v-for="i in 3" :key="i">
<el-row :gutter="20">
<el-col :span="24" v-for="i in 3" :key="i" class="video-wrapper">
<div class="item small" @click="() => {
activeIndex = videoList.length - 3 + i - 1;
}">
<img :src="videoList[videoList.length - 3 + i - 1].url" alt="">
<div class="title">{{ videoList[videoList.length - 3 + i - 1].name }}</div>
// 计算要显示的视频索引 - 按照当前视频后面三个排序
const displayIndex = (activeIndex + i) % videoList.length;
activeIndex = displayIndex;
}" :id="`smallVideo-expanded-${i}`">
<!-- <div class="title">{{ videoList[(activeIndex + i) % videoList.length].name }}
</div> -->
</div>
<!-- 小视频的两个按钮切换视图和查看视频 -->
<div class="video-action-btn expanded">
<el-button type="success" size="small" @click.stop="() => {
const displayIndex = (activeIndex + i) % videoList.length;
activeIndex = displayIndex;
// 不需要改变isExpanded因为已经在扩展布局
}">查看视频</el-button>
</div>
</el-col>
</el-row>
@ -52,22 +68,25 @@
<!-- 普通布局所有视频均匀排列 -->
<template v-else>
<el-col :span="8" v-for="(item, index) in videoList" :key="index">
<div class="item" @click="() => {
activeIndex = index;
isExpanded = true;
}">
<img :src="item.url" alt="">
<div class="title">{{ item.name }}</div>
<el-col :span="8" v-for="(item, index) in videoList" :key="index" class="video-wrapper">
<!-- 视频容器 -->
<div class="item" :id="`smallVideo-${index + 1}`" :ref="el => smallVideoRefs[index] = el">
<!-- <div class="title">{{ item.name }}</div> -->
</div>
<!-- 按钮放在最外层与视频容器同级 -->
<div class="video-action-btn">
<el-button type="primary" @click.stop="() => {
activeIndex = index;
isExpanded = true;
}">切换视图</el-button>
</div>
</el-col>
</template>
</el-row>
<el-row v-if="isExpanded">
<el-row v-if="!isExpanded">
<div class="pagination" v-if="activeTab !== 'record'">
<el-pagination layout="prev, pager, next, jumper, sizes" :total="totalRecords"
v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]"
<el-pagination layout="prev, pager, next, jumper" :total="totalRecords"
v-model:current-page="pageStart" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]"
@current-change="handlePageChange" @size-change="handleSizeChange"></el-pagination>
</div>
</el-row>
@ -76,63 +95,341 @@
</el-row>
</template>
<script setup>
import { ref } from 'vue';
<script setup lang="ts">
import { Refresh } from '@element-plus/icons-vue';
import TitleComponent from '@/components/TitleComponent';
const activeIndex = ref(-1); // 初始无选中,选中后为对应索引
const isExpanded = ref(false); // 初始为普通布局
const pageSize = ref(20);
const totalRecords = ref(100);
const videoList = ref([
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c144e8ba276a0e8a4a55c2.jpeg#bab7e2e06aae943525cacb13bd63e30d'
},
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c144efb5e8b987e5ca6462.jpeg#5523cf094b2f8c3a79ea4eb330c99a30'
},
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c144fbbad414f81995e90c.webp#230d8ca5ca39982518439db26e0ea899'
},
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c1450640d5d2a02e2540b2.webp#adad2379a0b04d6968364e4fb1133f77'
},
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c14543d56431f9d6f6808e.webp#16f0a0d8fab4f8ff3b39b04bfabac054'
},
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c14578d56431f9d6f68981.jpg#e77150417f28a971be4846eb0be90373'
},
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c145e03f22157da619a7ce.png#546ff44289a22bf175e1eca1f69cd8f9'
},
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c1461fb5e8b987e5caa293.jpeg#870e4d2b88b487ecb8f2f0b956c45c08'
},
{
name: 'A区d厂',
url: 'https://img.js.design/assets/img/68c1462dcbf9ed2271880b95.webp#ae7ae94ca84ce980e2d2281869335f06'
import EZUIKit from 'ezuikit-js';
// import TitleComponent from '@/components/TitleComponent';
import { getToken, getMonitoringList } from '@/api/securitySurveillance/index.js';
import { ref, onMounted, watch, nextTick, onUnmounted } from 'vue';
import { useUserStore } from '@/store/modules/user';
const activeIndex = ref(0); // 初始选中第一个视频
const isExpanded = ref(true); // 初始为扩展
const accessToken = ref('')
const pageStart = ref(1);
const pageSize = ref(4); // 默认请求4个视频扩展布局
const totalRecords = ref(0);
const activeTab = ref('live');
const bigVideoRef = ref<HTMLDivElement>(null);
const smallVideoRefs = ref<Array<HTMLDivElement | null>>([]); // 使用数组存储多个视频容器引用
const currentProject = computed(() => useUserStore().selectedProject);
const videoList = ref([]);
// 存储第二页的数据,用于处理扩展视图右边视频不足的情况
const nextPageVideoList = ref([]);
// 标记是否已经使用了下一页的数据
const hasUsedNextPageData = ref(false);
const StructureEZUIKitPlayer = (item: any, index: number, isBig = false) => {
// 添加输入参数的安全检查
if (!item || typeof item !== 'object') {
console.error('无效的视频项数据:', item);
return;
}
]);
const containerId = isBig ? 'bigVideo' : isExpanded.value ? `smallVideo-expanded-${index + 1}` : `smallVideo-${index + 1}`;
const container = document.getElementById(containerId);
// 先销毁旧的播放器实例(如果存在)
if (item.player) {
try {
// 添加安全检查确保destroy方法存在
if (typeof item.player.destroy === 'function') {
// 尝试移除所有事件监听器
if (item.player.off) {
item.player.off('*');
}
item.player.destroy();
}
}
catch (error) {
console.error('销毁播放器失败:', error);
}
item.player = null;
}
if (container && accessToken.value && item.deviceSerial) {
try {
item.player = new EZUIKit.EZUIKitPlayer({
audio: '0',
id: containerId,
accessToken: accessToken.value,
url: `ezopen://open.ys7.com/${item.deviceSerial}/1.hd.live`,
template: "pcLive",
width: container.clientWidth,
height: container.clientHeight,
plugin: ['talk']
});
} catch (error) {
console.error('创建播放器失败:', error);
item.player = null;
}
} else {
console.error(`创建播放器失败,缺少必要条件: container=${!!container}, accessToken=${!!accessToken.value}, deviceSerial=${!!item.deviceSerial}`);
}
};
// 获取萤石云Token
const getTokenData = async () => {
const { data } = await getToken()
accessToken.value = data
}
// 获取摄像头列表
const getMonitoringListData = async () => {
// 根据当前视图类型设置请求数量
const currentPageSize = isExpanded.value ? 4 : 9;
const { data: { object, sum }, } = await getMonitoringList({
pageStart: pageStart.value,
pageSize: currentPageSize,
isflow: true,
projectId: currentProject.value?.id,
})
totalRecords.value = Number(sum)
// 确保object是数组如果不是则使用空数组
videoList.value = Array.isArray(object) ? object : []
}
// 获取下一页视频数据
const getNextPageData = async () => {
const { data: { object, sum } } = await getMonitoringList({
pageStart: pageStart.value + 1,
isflow: true,
pageSize: 3 // 只需要3个视频
})
// 确保object是数组如果不是则使用空数组
nextPageVideoList.value = Array.isArray(object) ? object : [];
// 标记已经使用了下一页的数据
hasUsedNextPageData.value = true;
}
const getData = async () => {
// 先清理所有播放器
cleanupPlayers();
// 不再每次都获取tokentoken只在组件挂载时获取一次
await getMonitoringListData()
// 不再重置activeIndex保留用户之前选择的视频索引
// 等待DOM更新后初始化视频
await nextTick();
initVideo()
}
const initVideo = async () => {
// 先清理所有视频容器的内容,避免残留
if (bigVideoRef.value) {
bigVideoRef.value.innerHTML = '';
}
// 清理所有小视频容器的内容
smallVideoRefs.value.forEach(ref => {
if (ref) {
ref.innerHTML = '';
}
});
// 确保videoList是数组如果不是则使用空数组
const safeVideoList = Array.isArray(videoList.value) ? videoList.value : [];
// 确保nextPageVideoList是数组如果不是则使用空数组
const safeNextPageVideoList = Array.isArray(nextPageVideoList.value) ? nextPageVideoList.value : [];
if (isExpanded.value) {
// 扩展布局初始化大视频和右侧3个小视频
// 安全检查确保activeIndex在有效范围内
const safeActiveIndex = Math.min(Math.max(activeIndex.value, 0), safeVideoList.length - 1);
if (safeVideoList.length > 0 && safeVideoList[safeActiveIndex] && typeof safeVideoList[safeActiveIndex] === 'object') {
StructureEZUIKitPlayer(safeVideoList[safeActiveIndex], 0, true);
}
// 检查当前视频后面是否有足够的视频
const remainingVideos = safeVideoList.length - safeActiveIndex - 1;
if (remainingVideos >= 3) {
// 当前页后面有足够的视频,直接使用当前页的数据
for (let i = 0; i < 3; i++) {
const videoIndex = safeActiveIndex + 1 + i;
if (safeVideoList[videoIndex] && typeof safeVideoList[videoIndex] === 'object') {
// 修复索引使用i而不是i+1确保正确对应smallVideo-expanded-1, 2, 3
StructureEZUIKitPlayer(safeVideoList[videoIndex], i);
}
}
} else {
// 当前页后面视频不足3个需要获取下一页的数据
await getNextPageData();
// 重新获取安全的视频列表
const updatedSafeNextPageVideoList = Array.isArray(nextPageVideoList.value) ? nextPageVideoList.value : [];
// 使用当前页后面的视频和下一页的前几个视频
let displayCount = 0;
// 先显示当前页后面的视频
for (let i = safeActiveIndex + 1; i < safeVideoList.length && displayCount < 3; i++) {
if (safeVideoList[i] && typeof safeVideoList[i] === 'object') {
// 修复索引使用displayCount而不是displayCount+1
StructureEZUIKitPlayer(safeVideoList[i], displayCount);
displayCount++;
}
}
// 再显示下一页的视频补充到3个
for (let i = 0; i < updatedSafeNextPageVideoList.length && displayCount < 3; i++) {
if (updatedSafeNextPageVideoList[i] && typeof updatedSafeNextPageVideoList[i] === 'object') {
// 修复索引使用displayCount而不是displayCount+1
StructureEZUIKitPlayer(updatedSafeNextPageVideoList[i], displayCount);
displayCount++;
}
}
}
} else {
// 普通布局:如果之前在扩展视图中使用了下一页数据,现在需要更新页码
if (hasUsedNextPageData.value) {
pageStart.value += 1;
// 重新获取第二页的9个视频数据
await getMonitoringListData();
hasUsedNextPageData.value = false;
}
// 初始化所有视频,添加更严格的类型检查
safeVideoList.forEach((item, index) => {
if (item && typeof item === 'object') {
StructureEZUIKitPlayer(item, index);
} else {
console.warn(`跳过无效的视频项: ${index}`);
}
});
}
}
const handlePageChange = (page: number) => {
pageStart.value = page;
// 这里可以添加分页逻辑
getData()
}
const handleSizeChange = (size: number) => {
// 根据当前视图类型设置合适的页面大小
// 扩展视图固定为4普通视图固定为9
pageSize.value = isExpanded.value ? 4 : 9;
pageStart.value = 1;
getData()
}
// 清理所有播放器实例
const cleanupPlayers = () => {
// 清理当前页视频的播放器
videoList.value.forEach(item => {
if (item && item.player && typeof item.player.destroy === 'function') {
try {
// 尝试移除所有事件监听器
if (item.player.off) {
item.player.off('*');
}
// 销毁播放器实例
item.player.destroy();
}
catch (error) {
console.error('销毁播放器失败:', error);
}
// 确保播放器引用被清除
item.player = null;
}
});
// 清理下一页视频的播放器
nextPageVideoList.value.forEach(item => {
if (item && item.player && typeof item.player.destroy === 'function') {
try {
// 尝试移除所有事件监听器
if (item.player.off) {
item.player.off('*');
}
// 销毁播放器实例
item.player.destroy();
}
catch (error) {
console.error('销毁下一页视频播放器失败:', error);
}
// 确保播放器引用被清除
item.player = null;
}
});
}
// 监听isExpanded变化根据不同情况处理数据请求
watch(isExpanded, async (newValue, oldValue) => {
// 保存当前activeIndex的值
const currentActiveIndex = activeIndex.value;
// 从扩展视图切换到普通视图需要重新请求9个视频
if (newValue === false && oldValue === true) {
// 同步更新页面大小为9
pageSize.value = 9;
// 清理所有播放器实例
cleanupPlayers();
// 等待DOM更新
await nextTick();
// 重新请求9个视频数据
await getMonitoringListData();
// 再次等待DOM更新
await nextTick();
// 初始化视频
initVideo();
// 恢复保存的activeIndex值
activeIndex.value = currentActiveIndex;
}
// 从普通视图切换到扩展视图,不需要重新请求数据
else if (newValue === true && oldValue === false) {
// 同步更新页面大小为4
pageSize.value = 4;
// 清理所有播放器实例
cleanupPlayers();
// 等待DOM更新完成后再初始化视频
await nextTick();
// 再次等待确保DOM完全渲染
await new Promise(resolve => setTimeout(resolve, 100));
// 初始化视频
initVideo();
}
});
watch(activeIndex, (newIndex) => {
if (isExpanded.value) {
// 当activeIndex变化时重新初始化扩展布局中的视频
initVideo();
}
});
onMounted(() => {
// 组件挂载时获取一次token
getTokenData().then(() => {
// token获取成功后获取视频数据
getData();
}).catch(error => {
console.error('获取token失败:', error);
});
});
// 组件卸载时销毁所有播放器
onUnmounted(() => {
cleanupPlayers();
});
</script>
<style scoped lang="scss">
.video-container {
.el-col {
margin-bottom: 20px;
}
// 视频包装器样式
.video-wrapper {
position: relative;
// 确保按钮在视频容器上方
}
.video-container {
.item {
height: 220px;
margin-bottom: 20px;
position: relative;
border: 2px solid rgba(45, 119, 249, 1);
cursor: pointer;
// 确保子元素正确定位
overflow: hidden;
img {
width: 100%;
@ -150,12 +447,53 @@ const videoList = ref([
width: 100%;
bottom: 0;
color: #fff;
z-index: 15;
}
}
// 视频动作按钮样式(放在最外层)
.video-action-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 20;
pointer-events: none; // 防止按钮阻止鼠标悬停事件
}
// 右侧小视频的按钮容器样式
.video-action-btn.expanded {
display: flex;
gap: 10px;
}
// 鼠标悬停在视频包装器上时显示按钮
.video-wrapper:hover .video-action-btn {
opacity: 1;
}
// 鼠标悬停在视频上时添加半透明背景效果
.video-wrapper:hover .item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2);
z-index: 5;
}
// 按钮本身需要有点击事件
.video-action-btn button {
pointer-events: auto;
}
// 大视频样式(高度与右侧三个小视频总高度对齐,考虑间距)
.large {
height: calc(220px * 3 + 20px * 2); // 高度为3个小视频高度加上2个间距
height: calc(220px * 3 + 20px * 2 + 40px); // 高度为3个小视频高度加上2个间距
.tools {
display: flex;
@ -171,7 +509,7 @@ const videoList = ref([
// 小视频样式(保持原高度,适配右侧单列)
.small {
height: 220px;
height: 235px;
}
}
</style>

View File

@ -35,9 +35,11 @@
<div class="box" style="height: 100%;display: flex;">
<div class="left"
style="display: flex;flex-direction: column;height: 100%;justify-content: space-around;padding: 15px;">
<div style="color: rgba(102, 102, 102, 1);">今日录像时长</div>
<div style="color: rgba(102, 102, 102, 1);">今日设备情况</div>
<div><span
style="font-size: 30px;font-weight: 400;letter-spacing: 0px;line-height: 36px;color: rgba(0, 0, 0, 1);font-weight: bold;">54</span>
style="font-size: 30px;font-weight: 400;letter-spacing: 0px;line-height: 36px;color: rgba(0, 0, 0, 1);font-weight: bold;">{{
data?.sumMon
|| 0}}</span>
<span
style="font-size: 12px;font-weight: 400;letter-spacing: 0px;line-height: 17.38px;color: rgba(154, 154, 154, 1);">
@ -48,14 +50,14 @@
style="width: 63px;height: 18px;border-radius: 16px;background: rgba(0, 184, 122, .3);text-align: center;">
<span
style="font-size: 12px;font-weight: 400;letter-spacing: 0;line-height: 17.38px;color: rgba(0, 184, 122, 1);">
正常<span>53</span>
正常<span>{{ data?.sumOnLine || 0 }}</span>
</span>
</div>
<div
style="width: 63px;height: 18px;border-radius: 16px;background: rgba(227, 39, 39, .3);text-align: center;margin-left: 10px;">
<span
style="font-size: 12px;font-weight: 400;letter-spacing: 0;line-height: 17.38px;color: rgba(0, 184, 122, 1);color: red;">
异常<span>53</span>
异常<span>{{ data?.sumOffLine || 0 }}</span>
</span>
</div>
<!-- <div>异常<span>1</span></div> -->
@ -153,4 +155,11 @@
margin-right: 30px;
}
</style>
<script setup></script>
<script setup>
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
</script>

View File

@ -24,8 +24,8 @@
</el-row> <!-- 闭合内层 el-row -->
</el-col>
</el-row>
<el-row>
<Top />
<el-row style="margin-top: 20px;">
<Top :data="data" />
</el-row>
<el-row style="margin-top: 20px;" :gutter="25">
<el-col :span="18">
@ -37,7 +37,7 @@
</el-row>
<el-row style="margin-top: 20px;">
<el-col>
<Sbzt />
<Sbzt :data="data" />
</el-col>
</el-row>
</div>
@ -55,4 +55,11 @@ import Top from "./components/top"
import Spjk from "./components/spjk"
import Spgl from "./components/spgl";
import Sbzt from "./components/sbzt";
import { getHomeScreenData } from "@/api/securitySurveillance";
const data = ref(null)
onMounted(() => {
getHomeScreenData().then(res => {
data.value = res.data
})
})
</script>

View File

@ -1,5 +1,17 @@
<template>
<div class="detaildata-container">
<div class="title-container">
<div class="title-left">
<TitleComponent title="发电量同比分析" :font-level="2" />
</div>
<div class="title-right">
<el-input
placeholder="请输入搜索内容"
style="width: 200px;"
prefix-icon="Search"
/>
</div>
</div>
<el-table
v-loading="loading"
:data="tableData"
@ -177,8 +189,24 @@ onMounted(() => {
.detaildata-container {
padding: 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.title-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 16px;
}
.title-left {
display: flex;
align-items: center;
}
.title-right {
display: flex;
align-items: center;
}
.pagination-container {

View File

@ -1,6 +1,12 @@
<template>
<div class="duibifenxi-bar-container">
<div ref="chartRef" class="chart" style="width: 100%; height: 300px;"></div>
<div class="title">
<TitleComponent title="发电量同比分析" :font-level="2" />
<el-select placeholder="请选择线路" style="width: 150px;">
<el-option label="A线路" value="all"></el-option>
</el-select>
</div>
<div ref="chartRef" class="chart-container"></div>
</div>
</template>
@ -42,7 +48,7 @@ const initChart = () => {
axisPointer: {
type: 'shadow'
},
formatter: function(params: any) {
formatter: function (params: any) {
const current = params[0]
const lastYear = params[1]
let result = `${current.name}<br/>`
@ -78,16 +84,12 @@ const initChart = () => {
},
yAxis: {
type: 'value',
name: 'kwh',
nameTextStyle: {
color: '#666',
padding: [0, 0, 0, 40]
},
axisLine: {
show: false
},
axisLabel: {
color: '#666'
color: '#666',
formatter: '{value} Kwh',
},
splitLine: {
lineStyle: {
@ -147,28 +149,35 @@ onUnmounted(() => {
<style scoped lang="scss">
.duibifenxi-bar-container {
padding: 16px;
padding: 10px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
display: flex;
flex-direction: column;
min-height: 300px;
}
.chart {
flex: 1;
min-height: 0;
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
width: 100%;
}
.chart-container {
width: 100%;
height: 100%;
min-height: 280px;
}
// 响应式调整
@media screen and (max-width: 768px) {
.duibifenxi-bar-container {
padding: 12px;
padding: 5px;
min-height: 250px;
}
.chart {
height: 250px;
.chart-container {
min-height: 230px;
}
}
</style>

View File

@ -1,43 +1,45 @@
<template>
<div class="tongbifenxi-line-container">
<div id="tongbifenxiLineChart" class="chart-container"></div>
<div class="title">
<TitleComponent title="发电量同比分析" :font-level="2" />
<el-select placeholder="请选择线路" style="width: 150px;">
<el-option label="A线路" value="all"></el-option>
</el-select>
</div>
<div ref="chartDomRef" class="chart-container"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import * as echarts from 'echarts';
import TitleComponent from '@/components/TitleComponent/index.vue';
const chartDomRef = ref<HTMLElement | null>(null);
const chartInstance = ref<echarts.ECharts | null>(null);
const initChart = () => {
const chartDom = document.getElementById('tongbifenxiLineChart');
if (!chartDom) return;
if (!chartDomRef.value) return;
chartInstance.value = echarts.init(chartDom);
chartInstance.value = echarts.init(chartDomRef.value);
// 写死的数据
const dates = ['1号', '2号', '3号', '4号', '5号', '6号', '7号'];
const growthRates = ['1.50', '1.20', '0.50', '0.80', '0.90', '0.30', '-2.00'];
const growthRates = [1.50, 1.20, 0.50, 0.80, 0.90, 0.30, -2.00];
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderColor: '#409eff',
trigger: 'item',
backgroundColor: '#67c23a',
borderWidth: 0,
textStyle: {
color: '#fff'
color: '#fff',
fontSize: 14
},
formatter: (params: any) => {
const data = params[0];
return `${data.name}:\n环比增长率: ${data.value}%`;
return `${params.name}\n环比增长率${params.value}%`;
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
padding: [10, 15]
},
grid: {
left: '3%',
@ -51,7 +53,7 @@ const initChart = () => {
boundaryGap: false,
data: dates,
axisTick: {
alignWithLabel: true
show: false
},
axisLine: {
lineStyle: {
@ -90,7 +92,6 @@ const initChart = () => {
{
name: '环比增长率',
type: 'line',
stack: 'Total',
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
@ -103,9 +104,6 @@ const initChart = () => {
}
])
},
emphasis: {
focus: 'series'
},
lineStyle: {
color: '#67c23a',
width: 3
@ -117,6 +115,17 @@ const initChart = () => {
borderColor: '#fff',
borderWidth: 2
},
emphasis: {
focus: 'series',
itemStyle: {
color: '#67c23a',
borderColor: '#fff',
borderWidth: 3,
shadowBlur: 10,
shadowColor: 'rgba(103, 194, 58, 0.5)'
},
},
data: growthRates,
smooth: true
}
@ -149,8 +158,14 @@ onBeforeUnmount(() => {
padding: 10px;
box-sizing: border-box;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
width: 100%;
}
.chart-container {

View File

@ -0,0 +1,200 @@
<template>
<div class="zonglan-container">
<!-- 循环生成统计卡片 -->
<div v-for="card in statCards" :key="card.id" class="stat-card">
<div class="card-header">
<span class="card-title">{{ card.title }}</span>
<el-tooltip content="查看详情" placement="top">
<el-icon>
<Warning />
</el-icon>
</el-tooltip>
</div>
<div class="card-content">
<div class="stat-value">{{ card.value }}</div>
<div class="stat-footer">
<span class="trend-indicator up">
<img src="/src/assets/demo/up.png" alt="up" class="trend-icon"> {{ card.trendChange }}
</span>
<el-select v-model="card.selectedTimeRange" placeholder="选择时间范围" style="width: 120px; font-size: 12px;">
<el-option label="Today" value="today"></el-option>
<el-option label="This Week" value="week"></el-option>
<el-option label="This Month" value="month"></el-option>
<el-option label="This Year" value="year"></el-option>
</el-select>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 统计卡片数据
interface StatCard {
id: string;
title: string;
value: string;
trendChange: string;
selectedTimeRange: string;
}
const statCards = ref<StatCard[]>([
{
id: 'power-total',
title: '总发电量',
value: '2,456.8',
trendChange: '4.2%',
selectedTimeRange: 'today'
},
{
id: 'year-on-year',
title: '同比增长率',
value: '3.8%',
trendChange: '0.5%',
selectedTimeRange: 'today'
},
{
id: 'month-on-month',
title: '环比增长率',
value: '2.1%',
trendChange: '0.3%',
selectedTimeRange: 'today'
},
{
id: 'efficiency',
title: '运行效率',
value: '98.6%',
trendChange: '1.2%',
selectedTimeRange: 'today'
}
]);
</script>
<style scoped>
.zonglan-container {
display: flex;
gap: 20px;
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.stat-card {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.card-title {
font-size: 14px;
color: #666;
font-weight: 500;
}
.info-icon {
color: #c0c4cc;
cursor: pointer;
font-size: 16px;
transition: color 0.3s;
}
.info-icon:hover {
color: #409eff;
}
.card-content {
position: relative;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
line-height: 1.2;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.stat-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.trend-indicator {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 500;
}
.trend-indicator.up {
color: #67c23a;
}
.trend-indicator.down {
color: #f56c6c;
}
.trend-indicator i {
margin-right: 4px;
font-size: 12px;
}
.trend-icon {
margin-right: 4px;
vertical-align: middle;
}
.time-range {
font-size: 12px;
color: #909399;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.zonglan-container {
gap: 15px;
}
.stat-card {
padding: 15px;
}
.stat-value {
font-size: 24px;
}
}
@media (max-width: 768px) {
.zonglan-container {
flex-direction: column;
gap: 12px;
}
.stat-card {
padding: 15px;
}
.stat-value {
font-size: 22px;
}
}
</style>

View File

@ -1,12 +1,81 @@
<template>
<div>
<DuibifenxiBar></DuibifenxiBar>
<tongbifenxiLine></tongbifenxiLine>
<detaildata></detaildata>
<div class="power-fenxi-container">
<!-- 标题栏 -->
<el-row :gutter="24">
<el-col :span="12">
<TitleComponent title="电量分析" subtitle="测量在电解过程中消耗的电荷量" />
</el-col>
<!-- 外层col控制整体宽度并右对齐同时作为flex容器 -->
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
<el-col :span="4">
<el-button type="primary">
导出数据
<el-icon class="el-icon--right">
<UploadFilled />
</el-icon>
</el-button>
</el-col>
</el-col>
</el-row>
<!-- 第一排总览组件 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="24">
<zonglan></zonglan>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="18">
<TitleComponent title="发电量对比分析" :font-level="2" />
</el-col>
<el-col :span="3">
<el-select placeholder="请选择时间" style="width: 100%;">
<el-option label="今天" value="all"></el-option>
</el-select>
</el-col>
<el-col :span="3">
<el-date-picker v-model="value1" type="daterange" range-separator="至" start-placeholder="开始"
end-placeholder="结束" style="width: 100%;" />
</el-col>
</el-row>
<el-row :gutter="20" class="mb-4">
<el-col :span="12">
<el-card>
<DuibifenxiBar></DuibifenxiBar>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<tongbifenxiLine></tongbifenxiLine>
</el-card>
</el-col>
</el-row>
<!-- 第三排详细数据组件 -->
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<detaildata></detaildata>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import TitleComponent from '@/components/TitleComponent/index.vue';
import detaildata from '@/views/shengchanManage/powerfenxi/components/detaildata.vue'
import tongbifenxiLine from './components/tongbifenxiLine.vue';
import DuibifenxiBar from './components/duibifenxiBar.vue';
import tongbifenxiLine from '@/views/shengchanManage/powerfenxi/components/tongbifenxiLine.vue';
import DuibifenxiBar from '@/views/shengchanManage/powerfenxi/components/duibifenxiBar.vue';
import zonglan from '@/views/shengchanManage/powerfenxi/components/zonglan.vue';
</script>
<style scoped>
.power-fenxi-container {
padding: 20px;
background-color: rgba(242, 248, 252, 1);
}
.mb-4 {
margin-bottom: 20px;
}
</style>

View File

@ -2,7 +2,7 @@
<div>
<div class="operation-inspection">
<!-- 导航标签 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab active" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,7 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<!-- 子选项卡 -->
<div class="tabs-wrapper">
@ -48,8 +48,8 @@
</el-select>
</div>
<div class="filter-actions">
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button type="primary" icon="el-icon-plus" class="create-btn" @click="handleCreate">手动创建计划</el-button>
<el-button type="primary" icon="Search" class="search-btn" @click="handleSearch"> 搜索 </el-button>
<el-button type="primary" icon="Plus" class="create-btn" @click="handleCreate">手动创建计划</el-button>
</div>
</div>
@ -312,43 +312,101 @@
v-model="detailDialogVisible"
title="巡检计划详情"
width="800px"
class="detail-dialog"
center
:show-close="true"
custom-class="beautified-detail-dialog"
:before-close="handleCloseDetailDialog"
class="custom-experiment-dialog"
>
<div class="detail-content">
<div class="detail-header">
<h3 class="detail-title">{{ detailData.planName || '巡检计划' }}</h3>
<el-tag :type="detailData.status === '1' ? 'success' : 'info'" class="detail-status-tag">
{{ detailData.status === '1' ? '启用' : detailData.status === '2' ? '停用' : '-' }}
</el-tag>
<div class="task-detail-container">
<!-- 基础信息区 -->
<div class="detail-card">
<h3 class="card-title">基础信息</h3>
<div class="card-content">
<div class="info-row">
<div class="info-item">
<span class="info-label">计划名称</span>
<span class="info-value">{{ detailData.planName || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value task-status">
<el-tag :type="detailData.status === '1' ? 'success' : 'info'">
{{ detailData.status === '1' ? '启用' : detailData.status === '2' ? '停用' : '-' }}
</el-tag>
</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">计划类型</span>
<span class="info-value">{{ getPlanTypeText(detailData.planType) || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">巡检对象</span>
<span class="info-value">{{ getObjectTypeText(detailData.objectType) || '-' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">巡检频率</span>
<span class="info-value">{{ detailData.inspectionFrequency || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">负责人</span>
<span class="info-value">{{ detailData.nickName || '-' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">开始日期</span>
<span class="info-value">{{ formatDate(detailData.beginTime) || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">结束日期</span>
<span class="info-value">{{ formatDate(detailData.endTime) || '-' }}</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">计划开始时间</span>
<span class="info-value">{{ detailData.planBeginTime || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">持续时间</span>
<span class="info-value">{{ detailData.duration || '-' }}分钟</span>
</div>
</div>
<div class="info-row">
<div class="info-item">
<span class="info-label">巡检项</span>
<div class="info-value">
<span v-for="(item, index) in detailData.itemVoList" :key="item.id" class="inspection-item-tag">
{{ item.name }}
<span v-if="index < detailData.itemVoList.length - 1" class="item-separator"></span>
</span>
<span v-if="!detailData.itemVoList || detailData.itemVoList.length === 0">-</span>
</div>
</div>
<div class="info-item">
<span class="info-label">电站ID</span>
<span class="info-value">{{ detailData.projectId || '-' }}</span>
</div>
</div>
</div>
</div>
<div class="detail-main">
<el-descriptions :column="{ xs: 1, sm: 1, md: 2, lg: 2 }" class="detail-descriptions" border>
<el-descriptions-item label="计划类型" class="detail-item">{{ getPlanTypeText(detailData.planType) || '-' }}</el-descriptions-item>
<el-descriptions-item label="巡检对象" class="detail-item">{{ getObjectTypeText(detailData.objectType) || '-' }}</el-descriptions-item>
<el-descriptions-item label="巡检频率" class="detail-item">{{ detailData.inspectionFrequency || '-' }}</el-descriptions-item>
<el-descriptions-item label="负责人" class="detail-item">{{ detailData.nickName || '-' }}</el-descriptions-item>
<el-descriptions-item label="开始日期" class="detail-item">{{ formatDate(detailData.beginTime) || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束日期" class="detail-item">{{ formatDate(detailData.endTime) || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划开始时间" class="detail-item">{{ detailData.planBeginTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="持续时间" class="detail-item">{{ detailData.duration || '-' }}分钟</el-descriptions-item>
<el-descriptions-item label="巡检项ID" class="detail-item">{{ detailData.inspectionItemId || '-' }}</el-descriptions-item>
<el-descriptions-item label="电站ID" class="detail-item">{{ detailData.projectId || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="detailData.remark" class="detail-remark">
<h4 class="remark-title">备注信息</h4>
<p class="remark-content">{{ detailData.remark }}</p>
<!-- 备注信息 -->
<div v-if="detailData.remark" class="detail-card">
<h3 class="card-title">备注信息</h3>
<div class="card-content">
<div class="description-content">
{{ detailData.remark }}
</div>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDetailDialog" class="close-btn">关闭</el-button>
<el-button @click="closeDetailDialog">关闭</el-button>
</span>
</template>
</el-dialog>
@ -605,13 +663,14 @@ const formatDate = (dateString) => {
const getUsersList = async () => {
try {
const response = await xunjianUserlist();
const userRows = response?.data?.rows || response?.rows || [];
// 适配新接口格式检查code为200且rows为数组
const userRows = response.code === 200 && response.rows && Array.isArray(response.rows) ? response.rows : [];
userList.value = userRows
.filter((item) => item && typeof item === 'object')
.map((item, index) => ({
label: item.userName || `用户${index + 1}`,
value: item.id || `id_${index}`
.map((item) => ({
label: item.userName || '未知用户',
value: String(item.userId || '') // 使用userId作为唯一标识
}));
if (userList.value.length === 0) {
@ -979,6 +1038,8 @@ const handleInspectionManagement3 = () => {
</script>
<style scoped>
@import url('./css/detail-dialog.css');
@import url('./css/step-bars.css');
.operation-inspection {
padding: 20px;
background-color: #f5f7fa;
@ -1122,47 +1183,127 @@ const handleInspectionManagement3 = () => {
color: #f56c6c;
}
.detail-dialog .el-dialog__body {
/* 弹窗样式 */
.create-plan-dialog .el-dialog__body {
padding: 24px;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
/* 详情弹窗样式 - 与工单列表页面保持一致 */
.custom-experiment-dialog .el-dialog__body {
max-height: 60vh;
overflow-y: auto;
padding: 24px;
}
.task-detail-container {
padding: 10px 0;
}
/* 详情卡片样式 */
.detail-card {
background-color: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #f0f2f5;
}
.detail-title {
font-size: 18px;
font-weight: bold;
color: #303133;
.card-title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
}
.detail-status-tag {
padding: 4px 12px;
.card-content {
padding: 0 4px;
}
/* 信息行和信息项样式 */
.info-row {
display: flex;
margin-bottom: 16px;
flex-wrap: wrap;
}
.info-item {
flex: 0 0 50%;
margin-bottom: 12px;
display: flex;
align-items: flex-start;
}
.info-item.full-width {
flex: 0 0 100%;
}
.info-label {
font-weight: 500;
color: #86909c;
margin-right: 8px;
min-width: 80px;
flex-shrink: 0;
}
.info-value {
color: #4e5969;
flex: 1;
word-break: break-all;
font-size: 14px;
}
.detail-descriptions {
margin-bottom: 20px;
/* 骨架屏样式 */
.skeleton-loading {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-item .el-descriptions__label {
.skeleton-card {
background-color: #f5f5f5;
border-radius: 8px;
padding: 16px;
}
.skeleton-header {
height: 20px;
width: 30%;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 12px;
}
.skeleton-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-row {
height: 16px;
width: 100%;
background-color: #e0e0e0;
border-radius: 4px;
}
/* 优先级标签样式 */
.task-status {
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: #606266;
border: 1px solid transparent;
}
.remark-title {
font-weight: 500;
margin-bottom: 8px;
color: #303133;
}
.remark-content {
.description-content {
padding: 12px;
background-color: #f5f7fa;
background-color: #f9f9f9;
border-radius: 4px;
line-height: 1.6;
color: #4e5969;
font-size: 13px;
}
</style>

View File

@ -2,7 +2,7 @@
<div>
<div class="execution-records">
<!-- 顶部导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,10 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab active" @click="handleInspection7">运维组织</div>
</div>
<!-- 页面标题 -->
<TitleComponent title="运维组织模块" subtitle="实时监控人员状态、车辆状态和班组状态"></TitleComponent>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -182,7 +179,6 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
import router from '@/router';
import TitleComponent from './TitleComponent.vue';
import * as echarts from 'echarts'; // 导入ECharts
import renwuImage from '@/assets/images/renwu.png';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
<div>
<div class="execution-records">
<!-- 顶部导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,10 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab active" @click="handleInspection7">运维组织</div>
</div>
<!-- 页面标题 -->
<TitleComponent title="运维组织模块" subtitle="实时监控人员状态、车辆状态和班组状态"></TitleComponent>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -139,7 +136,6 @@
<script setup>
import { ref, computed } from 'vue';
import router from '@/router';
import TitleComponent from './TitleComponent.vue';
// 搜索和筛选条件
const searchKeyword = ref('');

View File

@ -0,0 +1,374 @@
/* 详情弹窗通用样式 */
/* 详情卡片样式 */
.detail-card {
margin-bottom: 20px;
padding: 20px;
background-color: #fafafa;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.card-title {
margin: 0 0 16px 0;
padding-bottom: 12px;
border-bottom: 2px solid #409eff;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.card-content {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 信息行和信息项样式 */
.info-row {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.info-item {
flex: 1;
min-width: 280px;
}
.info-item.full-width {
min-width: 100%;
}
.info-label {
display: inline-block;
width: 100px;
color: #606266;
font-weight: 500;
}
.info-value {
color: #303133;
word-break: break-word;
}
/* 步骤相关样式 - 详情弹窗专用 - 使用外部CSS样式 */
.task-detail-container .steps-container {
width: 100%;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fff;
}
.task-detail-container .step-item {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 15px;
background-color: #fafafa;
border-radius: 6px;
position: relative;
transition: all 0.3s ease;
}
.task-detail-container .step-item:hover {
background-color: #f5f7fa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.task-detail-container .step-number {
width: 32px;
height: 32px;
background-color: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 14px;
font-weight: bold;
flex-shrink: 0;
z-index: 1;
}
.task-detail-container .step-item:not(:last-child)::after {
content: '';
position: absolute;
top: 50px;
left: 16px;
width: 2px;
height: calc(100% + 5px);
background-color: #e4e7ed;
z-index: 0;
}
.task-detail-container .step-info {
flex: 1;
}
.task-detail-container .step-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 4px;
font-size: 14px;
}
.task-detail-container .step-purpose {
color: #606266;
margin-bottom: 4px;
font-size: 13px;
}
.task-detail-container .step-time,
.task-detail-container .step-finish-time,
.task-detail-container .step-remark {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
.task-detail-container .step-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
margin-top: 4px;
}
.step-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.step-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 4px;
font-size: 14px;
}
.step-purpose {
color: #606266;
margin-bottom: 4px;
font-size: 13px;
}
.step-time,
.step-finish-time,
.step-remark {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
/* 步骤状态样式 - 详情弹窗专用 */
.task-detail-container .step-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
margin-top: 4px;
}
/* 步骤状态样式 - 待执行 */
.task-detail-container .step-status.status-pending {
background-color: #e6f7ff;
color: #1677ff;
border: 1px solid #91d5ff;
}
/* 步骤状态样式 - 执行中 */
.task-detail-container .step-status.status-executing {
background-color: #fffbe6;
color: #fa8c16;
border: 1px solid #ffe58f;
}
/* 步骤状态样式 - 已完成 */
.task-detail-container .step-status.status-completed {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
/* 步骤状态样式 - 已延期 */
.task-detail-container .step-status.status-delayed {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
/* 通用状态颜色样式 */
.status-pending {
color: #e6a23c;
}
.status-executing {
color: #409eff;
}
.status-completed {
color: #67c23a;
}
.status-delayed {
color: #f56c6c;
}
.status-unknown {
color: #909399;
}
/* 加载状态样式 */
.loading-details {
padding: 20px 0;
}
/* 骨架屏加载 */
.skeleton-loading {
display: flex;
flex-direction: column;
gap: 20px;
}
.skeleton-card {
background-color: #f2f2f2;
border-radius: 8px;
padding: 20px;
animation: skeleton-loading 1.5s infinite;
}
.skeleton-header {
height: 24px;
width: 140px;
background-color: #e0e0e0;
border-radius: 4px;
margin-bottom: 16px;
}
.skeleton-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.skeleton-row {
height: 20px;
background-color: #e0e0e0;
border-radius: 4px;
}
.skeleton-row:nth-child(1) {
width: 70%;
}
.skeleton-row:nth-child(2) {
width: 90%;
}
.skeleton-row:nth-child(3) {
width: 60%;
}
@keyframes skeleton-loading {
0% {
opacity: 0.6;
}
50% {
opacity: 0.3;
}
100% {
opacity: 0.6;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.info-item {
min-width: 100%;
}
.info-row {
gap: 12px;
}
/* 步骤条响应式设计 */
.task-detail-container .steps-container {
padding: 10px;
}
.task-detail-container .step-item {
flex-direction: column;
align-items: flex-start;
padding: 10px;
margin-bottom: 10px;
}
.task-detail-container .step-item > * {
width: 100%;
margin-bottom: 10px;
margin-right: 0 !important;
}
.task-detail-container .step-number {
margin-bottom: 10px;
width: 24px;
height: 24px;
font-size: 12px;
}
.task-detail-container .step-item:not(:last-child)::after {
display: none;
}
}
/* 弹窗按钮样式 */
.dialog-footer .el-button {
padding: 10px 24px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.dialog-footer .el-button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
/* 其他相关样式 */
.fail-reason {
color: #f56c6c;
}
.no-info {
color: #909399;
font-style: italic;
padding: 10px 0;
}
.loading-state {
text-align: center;
padding: 80px 20px;
color: #6c757d;
font-size: 14px;
}
.loading-state i {
display: block;
font-size: 48px;
margin-bottom: 16px;
color: #1677ff;
}
.step-content {
padding: 30px 20px;
background-color: #fafafa;
border-radius: 8px;
margin-top: 20px;
}

View File

@ -0,0 +1,206 @@
/* 步骤容器样式 */
.steps-container {
width: 100%;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fff;
}
/* 单个步骤项样式 */
.step-item {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 15px;
background-color: #fafafa;
border-radius: 6px;
position: relative;
transition: all 0.3s ease;
}
/* 步骤项悬停效果 */
.step-item:hover {
background-color: #f5f7fa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 步骤序号样式 */
.step-number {
width: 32px;
height: 32px;
background-color: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 14px;
font-weight: bold;
flex-shrink: 0;
z-index: 1;
}
/* 步骤连接线样式 */
.step-item:not(:last-child)::after {
content: '';
position: absolute;
top: 50px;
left: 16px;
width: 2px;
height: calc(100% + 5px);
background-color: #e4e7ed;
z-index: 0;
}
/* 步骤内容样式 */
.step-content {
padding: 30px 20px;
background-color: #fafafa;
border-radius: 8px;
margin-top: 20px;
}
/* 步骤信息样式 */
.step-info {
flex: 1;
}
/* 步骤名称样式 */
.step-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 4px;
font-size: 14px;
}
/* 步骤目的样式 */
.step-purpose {
color: #606266;
margin-bottom: 4px;
font-size: 13px;
}
/* 步骤时间样式 */
.step-time,
.step-finish-time,
.step-remark {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
/* 添加步骤按钮样式 */
.add-step-btn {
color: #409eff;
display: block;
margin: 15px auto 0;
padding: 8px 16px;
border-radius: 4px;
transition: all 0.3s ease;
}
.add-step-btn:hover {
color: #66b1ff;
background-color: #ecf5ff;
}
/* 删除步骤按钮样式 */
.delete-step-btn {
color: #f56c6c;
flex-shrink: 0;
}
.delete-step-btn:hover {
color: #ff8590;
background-color: #fef0f0;
}
/* 步骤状态标签样式 */
.step-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
margin-top: 4px;
}
/* 步骤状态样式 - 待执行 */
.step-status.status-pending {
background-color: #e6f7ff;
color: #1677ff;
border: 1px solid #91d5ff;
}
/* 步骤状态样式 - 执行中 */
.step-status.status-executing {
background-color: #fffbe6;
color: #fa8c16;
border: 1px solid #ffe58f;
}
/* 步骤状态样式 - 已完成 */
.step-status.status-completed {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
/* 步骤状态样式 - 已延期 */
.step-status.status-delayed {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
/* 响应式设计 - 中等屏幕 */
@media (max-width: 1024px) {
.steps-container {
padding: 15px;
}
.step-item {
padding: 12px;
margin-bottom: 12px;
}
.step-number {
width: 28px;
height: 28px;
font-size: 13px;
margin-right: 12px;
}
}
/* 响应式设计 - 小屏幕 */
@media (max-width: 768px) {
.steps-container {
padding: 10px;
}
.step-item {
flex-direction: column;
align-items: flex-start;
padding: 10px;
margin-bottom: 10px;
}
.step-item > * {
width: 100%;
margin-bottom: 10px;
margin-right: 0 !important;
}
.step-number {
margin-bottom: 10px;
width: 24px;
height: 24px;
font-size: 12px;
}
.step-item:not(:last-child)::after {
display: none;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
<div>
<div class="box-container">
<!-- 导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab active" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,7 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<div class="main-content">
<!-- 左侧日历区域 -->
<div class="calendar-container">
@ -43,7 +43,7 @@
<div class="form-container">
<div class="form-header">
<h2>今日待办</h2>
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAddTaskDialog">添加</el-button>
<el-button type="primary" icon="Plus" @click="openAddTaskDialog">添加</el-button>
</div>
<!-- 待办事项列表 - 动态渲染 -->
@ -54,6 +54,7 @@
class="todo-item"
:class="{ 'important': item.taskLevel === '重要', 'completed': item.status === 2 }"
>
<el-checkbox class="todo-checkbox" :checked="item.status === 2" @change="handleStatusChange(item, $event)"></el-checkbox>
<div
class="todo-color-indicator"
:class="{
@ -63,7 +64,6 @@
completed: item.status === 2
}"
></div>
<el-checkbox class="todo-checkbox" :checked="item.status === 2" @change="handleStatusChange(item, $event)"></el-checkbox>
<div class="todo-content">
<div class="todo-main">
<div class="todo-title">{{ item.title }}</div>
@ -590,16 +590,6 @@ const handleInspection7 = () => {
min-height: 100vh;
}
/* 导航栏样式 */
.navigation-tabs {
display: flex;
margin-bottom: 20px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
padding: 2px;
}
/* 已完成任务的样式 */
.todo-color-indicator.completed {
background-color: #dcdfe6;
@ -609,7 +599,15 @@ const handleInspection7 = () => {
color: #909399;
text-decoration: line-through;
}
/* 导航栏样式 */
.navigation-tabs {
display: flex;
margin-bottom: 20px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
padding: 2px;
}
.nav-tab {
padding: 12px 24px;
cursor: pointer;
@ -849,13 +847,14 @@ const handleInspection7 = () => {
/* 悬停显示操作按钮 */
.todo-item:hover .todo-actions {
opacity: 1;
background: linear-gradient(to right, rgba(173, 216, 230, 0), rgb(64, 158, 255));
right: 0;
opacity: 0.8;
}
/* 内容区域平移以给按钮留出空间 */
/* 取消内容区域平移效果 */
.todo-item:hover .todo-content {
transform: translateX(-120px);
transform: none;
}
.action-icon {
@ -942,7 +941,7 @@ const handleInspection7 = () => {
background-color: #ff4d4f;
}
::v-deep .custom-date-cell {
:deep(.custom-date-cell) {
width: 100%;
height: 100%;
padding: 5px;
@ -983,13 +982,13 @@ const handleInspection7 = () => {
}
/* 穿透作用域,强制设置日历单元格为正方形 */
::v-deep .el-calendar-table td {
:deep(.el-calendar-table td) {
padding: 2px;
vertical-align: top;
width: 120px; /* 强制宽度 */
height: 120px; /* 强制高度(与宽度一致) */
}
::v-deep .el-calendar-day {
:deep(.el-calendar-day) {
padding: 0; /* 移除默认内边距 */
width: 100%;
height: 100%;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
<div>
<div class="operation-organization">
<!-- 顶部导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -10,10 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab active" @click="handleInspection7">运维组织</div>
</div>
<!-- 页面标题 -->
<TitleComponent title="运维组织模块" subtitle="实时监控人员状态、车辆状态和班组状态"></TitleComponent>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -133,11 +130,9 @@
<script setup>
import { ref, watch, onMounted } from 'vue';
import router from '@/router';
import TitleComponent from './TitleComponent.vue';
import * as echarts from 'echarts';
// 激活的选项卡
const activeTab = ref('personnel');
//
// 统计数据(保持原有数据不变)
const totalPersonnel = ref(36);
@ -449,36 +444,7 @@ const handleInspectionManagement3 = () => {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.custom-tabs {
padding-top: 1px;
}
.custom-tabs .el-tabs__header {
margin: 0 -20px;
padding: 0 20px;
border-bottom: 1px solid #e4e7ed;
}
.custom-tabs .el-tabs__nav-wrap::after {
height: 0;
}
.custom-tabs .el-tabs__item {
font-size: 14px;
color: #606266;
padding: 16px 20px;
margin-right: 20px;
}
.custom-tabs .el-tabs__item.is-active {
color: #165dff;
font-weight: 500;
border-bottom: 2px solid #165dff;
}
.custom-tabs .el-tabs__item:hover {
color: #165dff;
}
/* */
/* 内容容器样式 */
.content-container {

View File

@ -2,7 +2,7 @@
<div>
<div class="operation-inspection">
<!-- 1. 顶部导航选项卡对应原试验系统的外层导航 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab active" @click="handleInspection3">试验管理</div>
@ -10,7 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<!-- 选项卡和按钮组合 -->
<div class="tabs-wrapper">
@ -49,8 +49,8 @@
></el-date-picker>
</div>
<div class="action-buttons">
<el-button type="primary" class="search-btn"> 搜索 </el-button>
<el-button type="primary" class="create-btn" @click="openRecordDialog"> <i class="fas fa-plus"></i> 新增实验记录 </el-button>
<el-button type="primary" icon="Search" class="search-btn"> 搜索 </el-button>
<el-button type="primary" icon="Plus" class="create-btn" @click="openRecordDialog"> <i class="fas fa-plus"></i> 新增实验记录 </el-button>
</div>
</div>
@ -67,13 +67,7 @@
<el-table-column align="center" prop="type" label="巡检类型" width="120"></el-table-column>
<el-table-column align="center" prop="cycle" label="巡检周期" width="120"></el-table-column>
<el-table-column align="center" prop="dateRange" label="执行时间范围"></el-table-column>
<el-table-column align="center" prop="progress" label="完成进度" width="120">
<template #default="scope">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: scope.row.progress + '%', backgroundColor: getProgressColor(scope.row.status) }"></div>
</div>
</template>
</el-table-column>
<el-table-column align="center" prop="status" label="状态" width="100">
<template #default="scope">
<span :class="['status-tag', `status-${scope.row.status}`]">
@ -374,10 +368,10 @@
</el-form-item>
<el-form-item label="实验对象类型" class="form-item">
<el-select v-model="formData.testObject" placeholder="请选择实验对象类型" class="form-input">
<el-option label="1安全试验" value="1" />
<el-option label="2网络实验" value="2" />
<el-option label="3性能试验" value="3" />
<el-option label="4" value="4" />
<el-option label="安全试验" value="1" />
<el-option label="网络实验" value="2" />
<el-option label="性能试验" value="3" />
<el-option label="其他试验" value="4" />
</el-select>
</el-form-item>
</div>
@ -418,17 +412,6 @@
</el-select>
</el-form-item>
<!-- 试验步骤 -->
<el-form-item label="试验步骤" class="form-item" style="width: 100%">
<div class="steps-container">
<div class="step-item" v-for="(step, index) in formData.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.content" placeholder="输入试验步骤" />
</div>
<el-button type="text" size="small" class="add-step-btn" @click="addStep">添加步骤</el-button>
</div>
</el-form-item>
<!-- 所需设备与准备 -->
<el-form-item label="所需资源与设备" class="form-item" style="width: 100%">
<div class="equipment-list">
@ -471,99 +454,113 @@
:close-on-click-modal="false"
:close-on-press-escape="false"
class="custom-experiment-dialog"
center
>
<div class="detail-content">
<!-- 基础信息 -->
<div class="detail-section">
<h3 class="section-title">基础信息</h3>
<div class="detail-grid">
<div class="detail-item">
<label class="detail-label">计划名称:</label>
<span class="detail-value">{{ detailData.planName || '-' }}</span>
<div v-if="detailData" class="task-detail-container">
<!-- 基础信息卡片 -->
<div class="detail-card">
<h3 class="card-title">基础信息</h3>
<div class="card-content">
<div class="info-row">
<div class="info-item">
<label class="info-label">计划名称:</label>
<span class="info-value">{{ detailData.planName || '-' }}</span>
</div>
<div class="info-item">
<label class="info-label">计划编号:</label>
<span class="info-value">{{ detailData.planCode || '-' }}</span>
</div>
</div>
<div class="detail-item">
<label class="detail-label">计划编号:</label>
<span class="detail-value">{{ detailData.planCode || '-' }}</span>
<div class="info-row">
<div class="info-item">
<label class="info-label">实验对象:</label>
<span class="info-value">{{ getTestObjectText(detailData.testObject) || '-' }}</span>
</div>
<div class="info-item">
<label class="info-label">负责人:</label>
<span class="info-value">{{ detailData.person?.userName || '-' }}</span>
</div>
</div>
<div class="detail-item">
<label class="detail-label">实验对象:</label>
<span class="detail-value">{{ getTestObjectText(detailData.testObject) || '-' }}</span>
</div>
<div class="detail-item">
<label class="detail-label">负责人:</label>
<span class="detail-value">{{ detailData.person?.userName || '-' }}</span>
</div>
<div class="detail-item">
<label class="detail-label">开始时间:</label>
<span class="detail-value">{{ detailData.beginTime ? formatDate(detailData.beginTime) : '-' }}</span>
</div>
<div class="detail-item">
<label class="detail-label">结束时间:</label>
<span class="detail-value">{{ detailData.endTime ? formatDate(detailData.endTime) : '-' }}</span>
<div class="info-row">
<div class="info-item">
<label class="info-label">开始时间:</label>
<span class="info-value">{{ detailData.beginTime ? formatDate(detailData.beginTime) : '-' }}</span>
</div>
<div class="info-item">
<label class="info-label">结束时间:</label>
<span class="info-value">{{ detailData.endTime ? formatDate(detailData.endTime) : '-' }}</span>
</div>
</div>
</div>
</div>
<!-- 实验设备 -->
<div v-if="detailData.testDevice" class="detail-section">
<h3 class="section-title">实验设备</h3>
<div class="device-list">
<span v-for="(device, index) in detailData.testDevice.split(',')" :key="index" class="device-tag">
{{ device.trim() }}
</span>
</div>
</div>
<!-- 实验步骤 -->
<div v-if="detailData.testStep" class="detail-section">
<h3 class="section-title">实验步骤</h3>
<div class="steps-container">
<div v-for="(step, index) in detailData.testStep.split(',')" :key="index" class="step-item">
<div class="step-number">{{ index + 1 }}</div>
<div class="step-content">{{ step.trim() }}</div>
<div v-if="detailData.testDevice" class="detail-card">
<h3 class="card-title">实验设备</h3>
<div class="card-content">
<div v-for="(device, index) in detailData.testDevice.split(',')" :key="index" class="info-item">
<label class="info-label">设备{{ index + 1 }}:</label>
<span class="info-value">{{ device.trim() }}</span>
</div>
</div>
</div>
<!-- 实验信息 -->
<div class="detail-section">
<h3 class="section-title">实验信息</h3>
<div class="detail-textarea">
<label class="detail-label">实验说明:</label>
<div class="detail-text">{{ detailData.testInfo || '-' }}</div>
</div>
<div class="detail-textarea">
<label class="detail-label">实验设置:</label>
<div class="detail-text">{{ detailData.testSetting || '-' }}</div>
</div>
<div class="detail-textarea">
<label class="detail-label">解决方案:</label>
<div class="detail-text">{{ detailData.testSolutions || '-' }}</div>
<div class="detail-card">
<h3 class="card-title">实验信息</h3>
<div class="card-content">
<div class="info-item full-width">
<label class="info-label">实验说明:</label>
<div class="info-value">{{ detailData.testInfo || '-' }}</div>
</div>
<div class="info-item full-width">
<label class="info-label">实验设置:</label>
<div class="info-value">{{ detailData.testSetting || '-' }}</div>
</div>
<div class="info-item full-width">
<label class="info-label">解决方案:</label>
<div class="info-value">{{ detailData.testSolutions || '-' }}</div>
</div>
</div>
</div>
<!-- 参与人员 -->
<div v-if="detailData.persons && detailData.persons.length > 0" class="detail-section">
<h3 class="section-title">参与人员</h3>
<div class="participant-list">
<div v-for="(person, index) in detailData.persons" :key="person.id" class="participant-item">
<span class="participant-name">{{ person.userName }}</span>
<span class="participant-team">{{ person.teamName }}</span>
<div v-if="detailData.persons && detailData.persons.length > 0" class="detail-card">
<h3 class="card-title">参与人员</h3>
<div class="card-content">
<div v-for="(person, index) in detailData.persons" :key="person.id" class="info-row">
<div class="info-item">
<label class="info-label">姓名:</label>
<span class="info-value">{{ person.userName }}</span>
</div>
<div class="info-item">
<label class="info-label">团队:</label>
<span class="info-value">{{ person.teamName }}</span>
</div>
</div>
</div>
</div>
<!-- 巡检项目 -->
<div v-if="detailData.inspectionItemList && detailData.inspectionItemList.length > 0" class="detail-section">
<h3 class="section-title">巡检项目</h3>
<div class="inspection-list">
<div v-for="(item, index) in detailData.inspectionItemList" :key="item.id" class="inspection-item">
<span class="inspection-name">{{ item.name }}</span>
<span class="inspection-type">{{ item.type }}</span>
<div v-if="detailData.inspectionItemList && detailData.inspectionItemList.length > 0" class="detail-card">
<h3 class="card-title">巡检项目</h3>
<div class="card-content">
<div v-for="(item, index) in detailData.inspectionItemList" :key="item.id" class="info-row">
<div class="info-item">
<label class="info-label">项目名称:</label>
<span class="info-value">{{ item.name }}</span>
</div>
<div class="info-item">
<label class="info-label">项目类型:</label>
<span class="info-value">{{ item.type }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-else class="loading-details">
<el-skeleton :count="6" :columns="2" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDetailDialog = false">关闭</el-button>
@ -811,7 +808,11 @@ const formData = ref({
envRequirements: '',
manager: '',
participants: [], // 改为数组存储多选的用户ID
steps: [{ content: '' }, { content: '' }, { content: '' }],
steps: [
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' },
{ name: '', intendedPurpose: '', intendedTime: '' }
],
equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false },
@ -839,20 +840,14 @@ const userList = ref([]);
const getUsersList = async () => {
try {
const response = await xunjianUserlist();
const userRows =
response?.data?.rows && Array.isArray(response.data.rows)
? response.data.rows
: response?.rows && Array.isArray(response.rows)
? response.rows
: Array.isArray(response)
? response
: [];
// 适配新接口格式检查code为200且rows为数组
const userRows = response.code === 200 && response.rows && Array.isArray(response.rows) ? response.rows : [];
userList.value = userRows
.filter((item) => item && typeof item === 'object')
.map((item, index) => ({
label: item.userName || `用户${index + 1}`,
value: item.id || `id_${index}`
.map((item) => ({
label: item.userName || '未知用户',
value: String(item.userId || '') // 使用userId作为唯一标识
}));
if (userList.value.length === 0) {
@ -914,10 +909,6 @@ const handleSave = async () => {
personIds: formData.value.participants.join(','),
inspectionItems: '',
testSolutions: formData.value.riskMitigation,
testStep: formData.value.steps
.filter((step) => step.content.trim())
.map((step) => step.content)
.join(','),
testDevice: formData.value.equipments
.filter((equip) => equip.selected)
.map((equip) => equip.name)
@ -927,8 +918,9 @@ const handleSave = async () => {
id: editRecordId.value // 若后端用planId等需改为对应字段名
};
// 4. 调用接口
// 调用接口
let response;
if (editRecordId.value) {
// 编辑模式:调用更新接口
response = await updateshiyan(requestData);
@ -965,7 +957,6 @@ const resetForm = () => {
envRequirements: '', // 环境要求为空
manager: '', // 负责人为空
participants: [], // 参与人员为空数组
steps: [{ content: '' }, { content: '' }, { content: '' }], // 步骤内容为空
equipments: [
{ name: '服务器(型号:XYZ-9000)', selected: false },
{ name: '网络测试仪(型号:NT-5000)', selected: false },
@ -1041,24 +1032,6 @@ const handleEditRecord = async (row) => {
const recordDetail = detailResponse.data.rows?.[0] || detailResponse.data;
// 兼容两种数据结构可能在rows数组中也可能直接在data中
// 3. 处理testStep将逗号分隔的字符串转换为步骤数组
const steps = [];
if (recordDetail.testStep) {
// 拆分字符串(例如 "1. 213,2. 321" → ["1. 213", "2. 321"]
const stepItems = recordDetail.testStep.split(',');
stepItems.forEach((stepText) => {
// 移除序号前缀(如"1. "),只保留内容
const content = stepText.replace(/^\d+\.\s*/, '').trim();
if (content) {
steps.push({ content });
}
});
}
// 确保至少有3个步骤如果解析后为空
while (steps.length < 3) {
steps.push({ content: '' });
}
// 4. 处理testDevice将逗号分隔的字符串转换为设备数组
const equipments = [];
if (recordDetail.testDevice) {
@ -1104,7 +1077,6 @@ const handleEditRecord = async (row) => {
envRequirements: recordDetail.envRequirements || recordDetail.testSetting || '',
manager: recordDetail.manager || recordDetail.personCharge || '',
participants: participants, // 从personIds解析的数组
steps: steps, // 解析后的步骤数组
equipments: equipments, // 解析并合并后的设备数组
riskMitigation: recordDetail.riskMitigation || recordDetail.testSolutions || ''
};
@ -1134,7 +1106,18 @@ const handleEditRecord = async (row) => {
};
// 添加新步骤
const addStep = () => {
formData.value.steps.push({ content: '' });
formData.value.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
};
// 删除步骤
const deleteStep = (index) => {
// 确保至少保留一个步骤
if (formData.value.steps.length <= 1) {
ElMessage.warning('至少需要保留一个步骤');
return;
}
// 从数组中删除指定索引的步骤
formData.value.steps.splice(index, 1);
};
// 添加新设备
@ -1231,10 +1214,24 @@ const formatDate = (dateString) => {
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 日期时间格式化函数
const formatDateTime = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
</script>
<style scoped>
/* 1. 基础容器样式(继承试验系统) */
@import url('./css/detail-dialog.css');
.operation-inspection {
padding: 20px;
background-color: #f9fbfd;
@ -1276,7 +1273,7 @@ const formatDate = (dateString) => {
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
}
/* 3. 页面标题(与试验系统一致) */
/* 3. 页面标题 */
.page-header {
margin-bottom: 20px;
}
@ -1908,53 +1905,6 @@ const formatDate = (dateString) => {
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
}
/* 试验步骤样式 */
.steps-container {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
width: 100%;
}
.step-item {
display: flex;
align-items: center;
margin-bottom: 16px;
width: 100%;
}
.step-item:last-child {
margin-bottom: 0;
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #165dff;
color: white;
font-size: 16px;
font-weight: 600;
margin-right: 16px;
flex-shrink: 0;
}
.step-input:focus {
border-color: #165dff;
outline: none;
}
.add-step-btn {
color: #165dff;
margin-top: 12px;
width: 100%;
text-align: center;
font-size: 14px;
}
/* 设备列表样式 */
.equipment-list {
border: 1px solid #e4e7ed;
@ -2013,7 +1963,7 @@ const formatDate = (dateString) => {
border-color: #0d47a1;
}
/* 响应式设计 */
/* 响应式设计 - 保留必要的覆盖样式 */
@media (max-width: 768px) {
.custom-experiment-dialog {
width: 90% !important;
@ -2033,222 +1983,13 @@ const formatDate = (dateString) => {
.new-equipment-input {
width: 100%;
}
}
/* 详情弹窗样式 */
.custom-experiment-dialog .el-dialog__body {
padding: 20px;
overflow: hidden;
}
.detail-content {
max-height: 600px;
overflow-y: auto;
padding-right: 8px;
}
/* 详情区块 */
.detail-section {
margin-bottom: 24px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #ffffff;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1890ff;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e8f4ff;
}
/* 基础信息网格 */
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-label {
font-size: 13px;
font-weight: 500;
color: #6c757d;
}
.detail-value {
font-size: 14px;
color: #2c3e50;
padding: 4px 0;
}
/* 文本区域 */
.detail-textarea {
margin-bottom: 16px;
}
.detail-text {
font-size: 14px;
color: #495057;
line-height: 1.6;
padding: 8px 0;
min-height: 60px;
white-space: pre-wrap;
word-break: break-word;
}
/* 设备列表样式 */
.device-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.device-tag {
display: inline-block;
padding: 6px 12px;
background-color: #f0f9ff;
color: #1890ff;
border: 1px solid #bae7ff;
border-radius: 16px;
font-size: 13px;
}
/* 步骤条样式 */
.steps-container {
padding-left: 8px;
}
.step-item {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
position: relative;
}
.step-item:last-child {
margin-bottom: 0;
}
.step-item:not(:last-child)::after {
content: '';
position: absolute;
left: 17px;
top: 36px;
bottom: -16px;
width: 2px;
background-color: #e4e7ed;
z-index: 1;
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #1890ff;
color: white;
font-size: 14px;
font-weight: 600;
margin-right: 16px;
flex-shrink: 0;
z-index: 2;
}
.step-content {
flex: 1;
padding: 8px 16px;
background-color: #fafafa;
border-radius: 6px;
font-size: 14px;
color: #2c3e50;
line-height: 1.5;
}
/* 列表样式 */
.participant-list,
.inspection-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.participant-item,
.inspection-item {
display: flex;
align-items: center;
gap: 24px;
padding: 12px 16px;
background-color: #f8f9fa;
border-radius: 8px;
}
.participant-name,
.inspection-name {
font-size: 14px;
font-weight: 500;
color: #2c3e50;
min-width: 120px;
}
.participant-team,
.participant-role,
.inspection-type {
font-size: 13px;
color: #6c757d;
}
.participant-item,
.inspection-item {
display: flex;
align-items: center;
gap: 24px;
padding: 12px 16px;
background-color: #f8f9fa;
border-radius: 8px;
}
.participant-name,
.inspection-name {
font-size: 14px;
font-weight: 500;
color: #2c3e50;
min-width: 120px;
}
.participant-team,
.participant-role,
.inspection-type {
font-size: 13px;
color: #6c757d;
}
/* 详情弹窗响应式设计 */
@media (max-width: 768px) {
.detail-grid {
grid-template-columns: 1fr;
}
.participant-item,
.inspection-item {
.info-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.participant-name,
.inspection-name {
min-width: auto;
.info-item {
min-width: 100%;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
<div>
<div class="inspection-tasks">
<!-- 导航栏 -->
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab" @click="handleInspection2">巡检管理</div>
<div class="nav-tab active" @click="handleInspection3">试验管理</div>
@ -10,7 +10,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -29,7 +29,7 @@
<el-option label="待执行" value="1"></el-option>
<el-option label="执行中" value="4"></el-option>
<el-option label="已延期" value="2"></el-option>
<!-- 接口暂停对应页面已延期 -->
<el-option label="已完成" value="5"></el-option>
<el-option label="失败" value="3"></el-option>
</el-select>
@ -49,8 +49,8 @@
</el-select>
</div>
<div class="filter-actions">
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button type="primary" icon="el-icon-plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
<el-button type="primary" icon="Search" class="search-btn" @click="handleSearch"> 搜索 </el-button>
<el-button type="primary" icon="Plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
</div>
</div>
</div>
@ -72,49 +72,83 @@
</div>
<div class="task-details">
<div class="detail-item">
<span class="detail-label">计划时间</span>
<span class="detail-value">{{ task.planTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">测试对象</span>
<span class="detail-value">{{ task.target }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行人</span>
<span class="detail-value">{{ task.executor }}</span>
</div>
<div class="detail-item">
<span class="detail-label">关联计划</span>
<span class="detail-value">{{ task.relatedPlan }}</span>
</div>
<!-- 已延期暂停原因 -->
<div v-if="task.status === '2'" class="delay-reason">
<span class="detail-label">延期原因</span>
<span class="detail-value">{{ task.delayReason || '未填写' }}</span>
</div>
<!-- 执行中进度 -->
<div v-if="task.status === '4'" class="progress-container">
<span class="detail-label">完成进度</span>
<div class="progress-bar" style="flex: 1; padding-left: 0; margin-top: 0">
<el-progress :percentage="task.progress || 0" stroke-width="6" :stroke-color="task.progressColor"></el-progress>
<!-- 失败卡片特殊展示 -->
<div v-if="task.status === '3'" class="failed-task-details">
<div class="detail-item">
<span class="detail-label">失败时间</span>
<span class="detail-value">{{ task.failTime || '未记录' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">试验阶段</span>
<span class="detail-value">{{ task.testStage || '未记录' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行人</span>
<span class="detail-value">{{ task.executor }}</span>
</div>
<div class="detail-item failed-reason-item">
<span class="detail-label">失败原因</span>
<span class="detail-value failed-reason">{{ task.originalData?.failReason || '未填写' }}</span>
</div>
</div>
<!-- 已完成/失败结果 -->
<div v-if="task.status === '5' || task.status === '3'" class="task-result">
<span class="detail-label">结果</span>
<span class="detail-value" :class="task.resultClass">{{ task.result }}</span>
<!-- 其他状态的卡片展示 -->
<div v-else>
<div class="detail-item">
<span class="detail-label">计划时间</span>
<span class="detail-value">{{ task.planTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">测试对象</span>
<span class="detail-value">{{ task.target }}</span>
</div>
<div class="detail-item">
<span class="detail-label">执行人</span>
<span class="detail-value">{{ task.executor }}</span>
</div>
<div class="detail-item">
<span class="detail-label">关联计划</span>
<span class="detail-value">{{ task.relatedPlan }}</span>
</div>
<!-- 已延期暂停原因 -->
<div v-if="task.status === '2'" class="delay-reason">
<span class="detail-label">延期原因</span>
<span class="detail-value">{{ task.delayReason || '未填写' }}</span>
</div>
<!-- 执行中进度 -->
<div v-if="task.status === '4'" class="progress-container">
<span class="detail-label">完成进度</span>
<div class="progress-bar" style="flex: 1; padding-left: 0; margin-top: 0">
<el-progress :percentage="task.progress || 0" stroke-width="6" :stroke-color="task.progressColor"></el-progress>
</div>
</div>
<!-- 已完成结果 -->
<div v-if="task.status === '5'" class="task-result">
<span class="detail-label">结果</span>
<span class="detail-value" :class="task.resultClass">{{ task.result }}</span>
</div>
</div>
</div>
<div class="task-actions">
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
{{ task.actionText }}
</el-button>
<!-- 失败卡片的特殊操作按钮 -->
<div v-if="task.status === '3'" class="failed-task-actions">
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
{{ task.actionText }}
</el-button>
</div>
<!-- 其他状态的操作按钮 -->
<div v-else>
<el-button type="text" class="action-btn view-btn" @click="handleView(task)"> 详情 </el-button>
<el-button type="primary" :class="task.actionClass" @click="handleAction(task)">
{{ task.actionText }}
</el-button>
</div>
</div>
</div>
</div>
@ -137,7 +171,7 @@
</div>
<!-- 添加新任务弹窗 -->
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="500px" :before-close="handleCancelCreateTask">
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="750px" :before-close="handleCancelCreateTask">
<el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" />
@ -192,6 +226,27 @@
style="width: 100%"
/>
</el-form-item>
<!-- 步骤条区域 -->
<el-form-item label="执行步骤" prop="steps">
<div class="steps-container">
<div class="step-item" v-for="(step, index) in createTaskForm.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.name" placeholder="输入步骤名称" style="flex: 1; margin-right: 10px" />
<el-input v-model="step.intendedPurpose" placeholder="输入预期目的" style="flex: 1; margin-right: 10px" />
<el-date-picker
v-model="step.intendedTime"
type="datetime"
placeholder="选择计划时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
style="width: 180px; margin-right: 10px"
/>
<el-button v-if="createTaskForm.steps.length > 1" type="text" @click="removeStep(index)" style="color: #f56c6c"> 删除 </el-button>
</div>
<el-button type="text" class="add-step-btn" @click="addStep">添加步骤</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@ -215,7 +270,7 @@
</div>
<div class="info-item">
<span class="info-label">任务状态</span>
<span class="info-value" :class="getStatusClass(detailData.status)">
<span class="info-value" :class="getTaskStatusClass(detailData.status)">
{{ getStatusText(detailData.status) }}
</span>
</div>
@ -260,7 +315,7 @@
</div>
<div class="info-item">
<span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.personInfo.phone }}</span>
<span class="info-value">{{ detailData.personInfo.phonenumber }}</span>
</div>
</div>
<div v-if="detailData.personInfo" class="info-row">
@ -268,10 +323,6 @@
<span class="info-label">性别</span>
<span class="info-value">{{ detailData.personInfo.sex === '1' ? '男' : '女' }}</span>
</div>
<div class="info-item">
<span class="info-label">民族</span>
<span class="info-value">{{ detailData.personInfo.nation }}</span>
</div>
</div>
<div v-else class="no-info">暂无执行人信息</div>
</div>
@ -307,6 +358,26 @@
</div>
</div>
<!-- 步骤条 -->
<div v-if="detailData.nodes && detailData.nodes.length > 0" class="detail-card">
<h3 class="card-title">执行步骤</h3>
<div class="steps-container">
<div v-for="(node, index) in detailData.nodes" :key="node.id || index" class="step-item">
<div class="step-number">{{ node.code || index + 1 }}</div>
<div class="step-info">
<div class="step-name">{{ node.name || '未命名步骤' }}</div>
<div class="step-purpose">{{ node.intendedPurpose || '无说明' }}</div>
<div class="step-time">计划时间{{ formatDateTime(node.intendedTime) }}</div>
<div v-if="node.finishTime" class="step-finish-time">完成时间{{ formatDateTime(node.finishTime) }}</div>
<div v-if="node.remark" class="step-remark">备注{{ node.remark }}</div>
</div>
<div class="step-status" :class="getStatusClass(node.status)">
{{ node.status === '2' ? '未完成' : '已完成' }}
</div>
</div>
</div>
</div>
<!-- 任务执行结果信息卡片如果有 -->
<div v-if="detailData.testFinal || detailData.failReason" class="detail-card">
<h3 class="card-title">执行结果信息</h3>
@ -335,19 +406,48 @@
</span>
</template>
</el-dialog>
<!-- 日志弹窗 -->
<el-dialog v-model="logsDialogVisible" title="任务执行日志" width="700px" :close-on-click-modal="false">
<div v-if="!logsLoading" class="logs-container">
<div v-if="logsData.length > 0" class="logs-list">
<div v-for="(log, index) in logsData" :key="index" class="log-item">
<div class="log-time">{{ log.timestamp || '-' }}</div>
<div class="log-content">{{ log.content || '-' }}</div>
</div>
</div>
<div v-else class="no-logs">
<el-empty description="暂无执行日志" />
</div>
</div>
<div v-else class="loading-logs">
<el-skeleton :count="5" class="log-skeleton" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="logsDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import { ref, computed, onMounted } from 'vue';
import router from '@/router';
// 引入已定义的接口函数
import { syrenwulist, syrenwuDetail, addsyrenwu, updatesyrenwu } from '@/api/zhinengxunjian/shiyan/renwu';
import { shiyanlist } from '@/api/zhinengxunjian/shiyan';
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index';
import { addjiedian, updatejiedian } from '@/api/zhinengxunjian/jiedian/index';
// 引入Element Plus组件提示/空状态/骨架屏/弹窗)
import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox } from 'element-plus';
import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox, ElDialog } from 'element-plus';
// 日志弹窗相关变量
const logsDialogVisible = ref(false);
const logsData = ref([]);
const logsLoading = ref(false);
/**
* 根据任务ID获取完整的任务详情数据
@ -379,6 +479,24 @@ const loading = ref(false);
// 筛选条件(与接口参数对应)
const taskStatus = ref(''); // 任务状态1=待执行2=暂停已延期3=失败4=执行中5=已完成
const planType = ref(''); // 关联计划ID1=每日2=每周3=每月
/**
* 将节点数据按模块分组
* @param {Array} nodes - 节点数据数组
* @returns {Array} 分组后的模块数组
*/
const groupNodesByModule = (nodes) => {
if (!nodes || !Array.isArray(nodes)) {
return [];
}
// 这里简单地将所有节点放在一个默认模块下实际应用中可以根据节点数据的module字段进行分组
const defaultGroup = {
module: '测试步骤',
items: nodes
};
return [defaultGroup];
};
const executor = ref('all'); // 执行人IDall=全部
// 用户列表通过xunjianUserlist接口获取
@ -420,15 +538,58 @@ const getStatusText = (status) => {
* @param {string} status - 任务状态码
* @returns {string} 样式类名
*/
/**
* 获取步骤状态对应的样式类
* @param {string|number} status - 步骤状态码
* @returns {string} 样式类名
*/
const getStatusClass = (status) => {
// 处理可能的数字输入
const statusStr = status?.toString() || '';
const statusClassMap = {
'1': 'status-pending',
'2': 'status-delayed',
'3': 'status-failed',
'4': 'status-running',
'5': 'status-completed'
'3': 'status-executing',
'4': 'status-completed'
};
return statusClassMap[status] || '';
return statusClassMap[statusStr] || 'status-unknown';
};
/**
* 格式化日期时间(用于步骤条)
* @param {string} dateTime - 日期时间字符串
* @returns {string} 格式化后的日期时间
*/
const formatDateTime = (dateTime) => {
if (!dateTime) return '-';
try {
const date = new Date(dateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (error) {
return dateTime;
}
};
/**
* 获取步骤状态文本
* @param {string|number} status - 步骤状态码
* @returns {string} 状态文本
*/
const getStepStatusText = (status) => {
const statusStr = status?.toString() || '';
const statusMap = {
'1': '待执行',
'2': '执行中',
'3': '已完成',
'4': '已延期'
};
return statusMap[statusStr] || '未知状态';
};
// 创建任务弹窗
@ -441,7 +602,8 @@ const createTaskForm = ref({
relatedPlan: '', // 关联计划ID接口testPlanId
executor: '', // 执行人ID接口person
workTimeRange1: null, // 工作时间段1
workTimeRange2: null // 工作时间段2
workTimeRange2: null, // 工作时间段2
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }] // 步骤数据数组
});
// 创建任务表单规则
const createTaskRules = {
@ -453,6 +615,21 @@ const createTaskRules = {
executor: [{ required: true, message: '请选择执行人', trigger: 'change' }]
};
// 添加步骤
const addStep = () => {
createTaskForm.value.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
};
// 删除步骤
const removeStep = (index) => {
// 确保至少保留一个步骤
if (createTaskForm.value.steps.length <= 1) {
ElMessage.warning('至少需要保留一个步骤');
return;
}
createTaskForm.value.steps.splice(index, 1);
};
// 构建timeInfo字符串
const getTaskTimeInfoString = () => {
const timeInfoArray = [];
@ -482,36 +659,13 @@ const getUsersList = async () => {
try {
const response = await xunjianUserlist({});
if (response.code === 200) {
// 从任务数据中提取用户信息
const usersMap = new Map(); // 使用Map确保id唯一
const tasks = response.rows || [];
// 直接从接口返回的用户列表中提取信息
const users = response.rows || [];
tasks.forEach((task) => {
// 提取personInfo中的用户信息
if (task.personInfo && task.personInfo.id && task.personInfo.userName) {
usersMap.set(task.personInfo.id, {
id: task.personInfo.id,
userName: task.personInfo.userName
});
}
// 提取testPlan.persons中的用户信息
if (task.testPlan && task.testPlan.persons && Array.isArray(task.testPlan.persons)) {
task.testPlan.persons.forEach((person) => {
if (person.id && person.userName) {
usersMap.set(person.id, {
id: person.id,
userName: person.userName
});
}
});
}
});
// 将Map转换为下拉选择器需要的格式{ label, value }
userList.value = Array.from(usersMap.values()).map((user) => ({
label: user.userName, // 显示在下拉框中的文本
value: user.id // 选中后的值
// 将用户数据转换为所需格式包含id和userName以适配模板和getUserById函数
userList.value = users.map((user) => ({
id: user.userId, // 用于标识和查找
userName: user.userName // 显示名称
}));
// 调试信息,确认数据格式正确
@ -604,9 +758,9 @@ const mapApiToView = (apiData) => {
},
'3': {
statusText: '失败',
cardClass: 'card-delayed',
tagClass: 'tag-delayed',
actionText: '重',
cardClass: 'card-failed',
tagClass: 'tag-failed',
actionText: '重新执行',
actionClass: 'reschedule-btn',
result: '失败',
resultClass: 'result-abnormal'
@ -663,6 +817,75 @@ const mapApiToView = (apiData) => {
executorName = getUserById(apiData.person);
}
// 格式化失败时间
const formatFailTime = (timeStr) => {
if (timeStr) {
const date = new Date(timeStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(
date.getHours()
).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
return '未记录';
};
// 生成试验阶段信息
const getTestStage = () => {
try {
// 优先查找nodes数组中处于执行中或失败的节点来确定当前试验阶段
if (apiData && apiData.nodes && Array.isArray(apiData.nodes)) {
// 查找执行中状态的节点
const executingNode = apiData.nodes.find((node) => {
if (!node || node.status === undefined) return false;
return node.status === '2' || node.status === 2;
});
// 如果有执行中的节点根据code判断阶段
if (executingNode && executingNode.code !== undefined) {
const stepName = executingNode.name || '未命名步骤';
return `${executingNode.code}步(${stepName})`;
}
// 查找失败状态的节点
const failedNode = apiData.nodes.find((node) => {
if (!node || node.status === undefined) return false;
return node.status === '3' || node.status === 3;
});
// 如果有失败的节点根据code判断阶段
if (failedNode && failedNode.code !== undefined) {
const stepName = failedNode.name || '未命名步骤';
return `${failedNode.code}步(${stepName})`;
}
// 查找已完成的节点,确定最后完成的阶段
const completedNodes = apiData.nodes.filter((node) => {
if (!node || node.status === undefined) return false;
return node.status === '4' || node.status === 4;
});
if (completedNodes.length > 0) {
// 按code排序取最大的code
completedNodes.sort((a, b) => Number(b.code) - Number(a.code));
if (completedNodes[0].code !== undefined) {
const stepName = completedNodes[0].name || '未命名步骤';
return `${completedNodes[0].code}步(${stepName})`;
}
}
}
// 如果没有找到符合条件的nodes数据检查是否有明确的试验阶段信息
if (apiData && apiData.testStage) {
return apiData.testStage;
}
// 如果没有明确的阶段信息,尝试从关联计划中获取
if (apiData && apiData.testPlan && apiData.testPlan.stage) {
return apiData.testPlan.stage;
}
} catch (error) {
console.error('获取试验阶段信息失败:', error);
}
return '未记录';
};
return {
id: apiData.id, // 任务IDv-for的key唯一标识
title: apiData.taskName || '未命名任务', // 任务名称
@ -682,7 +905,10 @@ const mapApiToView = (apiData) => {
actionText: statusConfig.actionText,
actionClass: statusConfig.actionClass,
testFinal: apiData.testFinal, // 结果(用于详情页)
originalData: apiData // 保存原始数据,用于后续操作
originalData: apiData, // 保存原始数据,用于后续操作
// 失败卡片特有字段
failTime: formatFailTime(apiData.failTime),
testStage: getTestStage()
};
};
@ -762,9 +988,18 @@ const handleAction = async (task) => {
id: task.id
};
// 声明resultType变量提升作用域
let resultType = null;
// 3. 根据任务状态只修改状态相关的字段
if (task.status === '4') {
// 执行中 → 完成:使用弹窗确认结果
try {
// 保持原有结构
} catch (error) {
console.error('捕获到异常:', error);
}
try {
const confirmResult = await ElMessageBox.confirm('请选择试验结果', '完成试验', {
confirmButtonText: '正常',
@ -776,12 +1011,72 @@ const handleAction = async (task) => {
updateParams.status = '5';
updateParams.progress = 100;
updateParams.testFinal = '正常';
resultType = 'normal'; // 现在在外部作用域中定义
} catch (error) {
if (error === 'cancel') {
// 用户点击取消(异常)
updateParams.status = '5';
updateParams.progress = 100;
updateParams.testFinal = '异常';
// 用户点击取消(异常),弹出失败原因输入框
try {
const failReasonResult = await ElMessageBox.prompt('请输入失败原因', '试验异常', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
inputPlaceholder: '请详细描述失败原因...',
inputValidator: (value) => {
if (!value || value.trim() === '') {
return '失败原因不能为空';
}
return true;
}
});
// 用户输入了失败原因并确认
updateParams.status = '3';
updateParams.progress = '';
updateParams.testFinal = '异常';
updateParams.failReason = failReasonResult.value; // 绑定失败原因参数
updateParams.failTime = formatLocalDateTime(new Date()); // 记录失败时间
resultType = 'abnormal';
// 将第一条未完成的步骤状态改为3失败
if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) {
const firstUnfinishedNode = taskDetails.nodes.find((node) => {
return node.status === '2' || node.status === 2;
});
if (firstUnfinishedNode) {
// 使用updatejiedian接口更新节点状态构造完整的节点信息数组
const nodeUpdateParams = [
{
...firstUnfinishedNode,
status: '3',
updateTime: new Date().toISOString(),
// 确保包含所有必需字段
createDept: firstUnfinishedNode.createDept || 0,
createBy: firstUnfinishedNode.createBy || 0,
createTime: firstUnfinishedNode.createTime || new Date().toISOString(),
updateBy: firstUnfinishedNode.updateBy || 0,
params: firstUnfinishedNode.params || {
property1: 'string',
property2: 'string'
},
module: firstUnfinishedNode.module || 'string',
orderId: firstUnfinishedNode.orderId || 0,
code: firstUnfinishedNode.code || 0,
name: firstUnfinishedNode.name || 'string',
intendedPurpose: firstUnfinishedNode.intendedPurpose || 'string',
intendedTime: firstUnfinishedNode.intendedTime || new Date().toISOString(),
finishTime: firstUnfinishedNode.finishTime || '',
remark: firstUnfinishedNode.remark || ''
}
];
await updatejiedian(nodeUpdateParams);
// 更新本地数据以反映最新状态
firstUnfinishedNode.status = '3';
}
}
} catch (innerError) {
// 用户取消了失败原因输入
return;
}
} else {
// 关闭弹窗,不执行操作
return;
@ -792,21 +1087,74 @@ const handleAction = async (task) => {
switch (task.status) {
case '1': // 待执行 → 开始执行状态改为4
updateParams.status = '4';
updateParams.progress = 10; // 初始进度10%
updateParams.progress = 0; // 初始进度10%
// 设置开始时间为当前时间使用本地时间而非UTC时间
updateParams.planBeginTime = formatLocalDateTime(new Date());
break;
case '2': // 已延期 → 重新安排状态改为1重置时间
updateParams.status = '1';
updateParams.beginTime = new Date().toISOString().slice(0, 16).replace('T', ' ');
updateParams.beginTime = formatLocalDateTime(new Date());
break;
case '3': // 失败 → 重试状态改为1
updateParams.status = '1';
// 清空失败相关字段,使用适合各字段数据类型的默认值
updateParams.failReason = '';
updateParams.failTime = ''; // 时间类型字段使用null
updateParams.failPhase = ''; // 整数类型字段使用0
// 将失败的步骤状态改回2未完成
if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) {
const failedNodes = taskDetails.nodes.filter((node) => {
return node.status === '3' || node.status === 3;
});
// 构造包含所有失败节点的完整信息数组
const nodeUpdateParams = failedNodes.map((failedNode) => ({
...failedNode,
status: '2',
updateTime: new Date().toISOString(),
// 确保包含所有必需字段
createDept: failedNode.createDept || 0,
createBy: failedNode.createBy || 0,
createTime: failedNode.createTime || new Date().toISOString(),
updateBy: failedNode.updateBy || 0,
params: failedNode.params || {
property1: 'string',
property2: 'string'
},
module: failedNode.module || 'string',
orderId: failedNode.orderId || 0,
code: failedNode.code || 0,
name: failedNode.name || 'string',
intendedPurpose: failedNode.intendedPurpose || 'string',
intendedTime: failedNode.intendedTime || new Date().toISOString(),
finishTime: failedNode.finishTime || '',
remark: failedNode.remark || ''
}));
// 一次性调用updatejiedian接口更新所有节点
await updatejiedian(nodeUpdateParams);
// 更新本地数据以反映最新状态
for (const failedNode of failedNodes) {
failedNode.status = '2';
}
}
break;
default:
return;
}
}
// 调用更新接口
// 对于执行中状态('4')的任务,预先设置好时间字段
if (task.status === '4') {
// 根据结果类型设置相应的时间使用本地时间而非UTC时间
if (resultType === 'normal') {
updateParams.planFinishTime = formatLocalDateTime(new Date());
} else if (resultType === 'abnormal') {
updateParams.failTime = formatLocalDateTime(new Date());
}
}
// 调用更新接口(只调用一次)
const response = await updatesyrenwu(updateParams);
if (response.code === 200) {
ElMessage.success(`任务${task.actionText}成功`);
@ -819,6 +1167,20 @@ const handleAction = async (task) => {
}
};
/**
* 格式化本地日期时间为 'YYYY-MM-DD HH:mm' 格式
* @param {Date} date - 日期对象
* @returns {string} 格式化后的日期时间字符串
*/
const formatLocalDateTime = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
/**
* 打开创建任务弹窗
*/
@ -854,6 +1216,48 @@ const handleSaveTask = async () => {
return;
}
// 验证所有步骤
const hasEmptyStep = createTaskForm.value.steps.some((step) => !step.name.trim() || !step.intendedPurpose.trim());
if (hasEmptyStep) {
ElMessage.warning('请填写完整所有步骤信息');
return;
}
// 处理步骤数据
let nodeIds = '';
if (createTaskForm.value.steps && createTaskForm.value.steps.length > 0) {
// 过滤非空步骤并映射为所需格式
const validSteps = createTaskForm.value.steps
.filter((step) => step.name.trim() && step.intendedPurpose.trim())
.map((step, index) => ({
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
params: {},
module: 3,
code: index + 1,
name: step.name,
intendedPurpose: step.intendedPurpose,
intendedTime: step.intendedTime ? new Date(step.intendedTime).toISOString() : new Date().toISOString(),
finishTime: '',
remark: '',
status: 2
}));
if (validSteps.length > 0) {
try {
// 调用addjiedian接口获取nodeIds
const jiedianResponse = await addjiedian(validSteps);
if (jiedianResponse.code === 200 && jiedianResponse.msg) {
nodeIds = jiedianResponse.msg; // 直接使用字符串格式,不转换为数组
}
} catch (error) {
console.error('添加节点失败:', error);
ElMessage.error('添加执行步骤失败');
return;
}
}
}
const createParams = {
createDept: 0, // 可根据实际情况从全局状态获取
createBy: 0, // 可根据实际情况从全局状态获取当前用户ID
@ -874,19 +1278,20 @@ const handleSaveTask = async () => {
status: '1', // 初始状态:待执行(必需)
testPlanId: createTaskForm.value.relatedPlan, // 关联计划ID必需
testSetting: '', // 测试设置
planBeginTime: createTaskForm.value.timeRange[0], // 计划开始时间
planBeginTime: '', // 计划开始时间(新增时为空)
progress: 0, // 初始进度0%
failReason: '',
failTime: now.toISOString(),
failPhase: 0,
failTime: '', // 失败时间(新增时为空)
failPhase: '',
faileAnalyze: '',
faileTips: '',
testLongTime: 0,
testFinal: '',
finalInfo: '',
pauseFor: '',
pauseTime: now.toISOString(),
planFinishTime: createTaskForm.value.timeRange[1] // 计划完成时间
pauseTime: '', // 暂停时间(新增时为空)
planFinishTime: '', // 计划完成时间(新增时为空)
nodeIds: nodeIds // 步骤节点ID数组
};
// 3. 调用创建接口
@ -927,7 +1332,10 @@ const handleCancelCreateTask = () => {
inspectionTarget: '',
timeRange: [],
relatedPlan: '',
executor: ''
executor: '',
workTimeRange1: null,
workTimeRange2: null,
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
};
};
@ -974,9 +1382,24 @@ onMounted(() => {
const pagedTasks = computed(() => {
return tasks.value;
});
// 获取任务状态对应的CSS类
const getTaskStatusClass = (status) => {
const statusStr = status?.toString() || '';
const statusMap = {
'1': 'status-pending',
'2': 'status-delayed',
'3': 'status-failed',
'4': 'status-running',
'5': 'status-completed'
};
return statusMap[statusStr] || 'status-pending';
};
</script>
<style scoped>
@import url('./css/step-bars.css');
@import url('./css/detail-dialog.css');
/* 原有样式不变,新增无数据提示样式 */
.inspection-tasks {
padding: 20px;
@ -1077,6 +1500,10 @@ const pagedTasks = computed(() => {
box-shadow: 0 4px 16px rgba(82, 196, 26, 0.15);
}
.card-failed {
box-shadow: 0 4px 16px rgba(255, 77, 79, 0.15);
}
/* 左侧状态线颜色 */
.card-pending::before {
background-color: #1677ff;
@ -1090,6 +1517,9 @@ const pagedTasks = computed(() => {
.card-completed::before {
background-color: #52c41a;
}
.card-failed::before {
background-color: #ff4d4f;
}
/* 卡片悬停效果 */
.task-card:hover {
@ -1146,6 +1576,12 @@ const pagedTasks = computed(() => {
border-color: #b7eb8f;
}
.tag-failed {
background-color: #fff2f0;
color: #ff4d4f;
border-color: #ffccc7;
}
.task-details {
margin-bottom: 16px;
}
@ -1229,24 +1665,26 @@ const pagedTasks = computed(() => {
color: #165dff;
}
.start-btn {
background-color: #165dff;
border-color: #165dff;
/* 失败卡片特殊样式 */
.failed-task-details {
margin-bottom: 16px;
}
.reschedule-btn {
background-color: #ff7d00;
border-color: #ff7d00;
.failed-reason-item {
padding-top: 8px;
border-top: 1px dashed #f0f2f5;
}
.complete-btn {
background-color: #00b42a;
border-color: #00b42a;
.failed-reason {
color: #f53f3f;
font-weight: 500;
}
.report-btn {
background-color: #86909c;
border-color: #86909c;
.failed-task-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
}
/* 分页区域样式 */
@ -1302,6 +1740,46 @@ const pagedTasks = computed(() => {
margin-bottom: 30px;
}
/* 日志弹窗样式 */
.logs-container {
max-height: 400px;
overflow-y: auto;
}
.logs-list {
padding: 10px 0;
}
.log-item {
padding: 12px 0;
border-bottom: 1px solid #f0f2f5;
}
.log-item:last-child {
border-bottom: none;
}
.log-time {
font-size: 12px;
color: #86909c;
margin-bottom: 4px;
}
.log-content {
font-size: 14px;
color: #1d2129;
line-height: 1.6;
}
.no-logs {
text-align: center;
padding: 60px 0;
}
.log-skeleton {
margin: 12px 0;
}
/* 任务详情弹窗样式 */
.task-detail-container {
max-height: 600px;
@ -1376,10 +1854,6 @@ const pagedTasks = computed(() => {
color: #e6a23c;
}
.status-running {
color: #409eff;
}
.status-completed {
color: #67c23a;
}

View File

@ -1,6 +1,6 @@
<template>
<div class="operation-inspection">
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab active" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -8,11 +8,11 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<div class="header-container">
<div class="header-actions">
<el-button type="primary" class="export-btn">筛选</el-button>
<el-button type="primary" class="create-btn">导出数据</el-button>
<el-button type="primary" icon="UploadFilled" class="create-btn">导出数据</el-button>
</div>
</div>
@ -54,7 +54,7 @@
></el-date-picker>
</div>
<div class="filter-actions">
<el-button type="primary" class="search-btn" @click="fetchDashboardData">搜索</el-button>
<el-button type="primary" icon="Search" class="search-btn" @click="fetchDashboardData">搜索</el-button>
</div>
</div>
@ -127,14 +127,14 @@
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">完成率</span>
<span class="text-gray-600">巡检完成率</span>
<span class="font-medium text-gray-800">{{ completionRate }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out" :style="{ width: completionRate + '%' }"></div>
</div>
</div>
<div>
<!-- <div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">解决率</span>
<span class="font-medium text-gray-800">{{ resolutionRate }}%</span>
@ -142,10 +142,10 @@
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div class="bg-red-500 h-2 rounded-full transition-all duration-1500 ease-out" :style="{ width: resolutionRate + '%' }"></div>
</div>
</div>
</div> -->
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">及时</span>
<span class="text-gray-600">解决效</span>
<span class="font-medium text-gray-800">{{ timelinessRate }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
@ -161,65 +161,8 @@
<!-- 发现问题种类 -->
<div class="py-4">
<h3 class="section-title">发现问题种类</h3>
<div class="space-y-4">
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">温度异常率</span>
<span class="text-gray-500">{{ problemTypes.temperature }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out"
:style="{ width: problemTypes.temperature + '%' }"
></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">内存使用率</span>
<span class="text-gray-500">{{ problemTypes.memory }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out"
:style="{ width: problemTypes.memory + '%' }"
></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">CPU负载</span>
<span class="text-gray-500">{{ problemTypes.cpu }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out" :style="{ width: problemTypes.cpu + '%' }"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">响应时间</span>
<span class="text-gray-500">{{ problemTypes.responseTime }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out"
:style="{ width: problemTypes.responseTime + '%' }"
></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">磁盘空间状态</span>
<span class="text-gray-500">{{ problemTypes.diskSpace }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
class="bg-blue-500 h-2 rounded-full transition-all duration-1500 ease-out"
:style="{ width: problemTypes.diskSpace + '%' }"
></div>
</div>
</div>
</div>
<!-- 柱状图容器 -->
<div id="problemTypesChart" class="bar-chart-container"></div>
</div>
</div>
</div>
@ -388,16 +331,17 @@ const avgCompletionTime = ref('45分钟');
// 问题类型数据
const problemTypes = ref({
temperature: 85, // 温度异常
memory: 62, // 内存使用率
cpu: 45, // CPU负载
responseTime: 30, // 响应时间
diskSpace: 15 // 磁盘空间状态
temperature: 0, // 温度异常数量
memory: 0, // 内存使用率问题数量
cpu: 0, // CPU负载问题数量
responseTime: 0, // 响应时间问题数量
diskSpace: 0 // 磁盘空间问题数量
});
// ECharts 图相关
// ECharts 图相关
const pieChartRef = ref(null);
let pieChart = null;
let barChart = null;
// 计算平均完成度
const averageRate = computed(() => (completionRate.value + resolutionRate.value + timelinessRate.value) / 3);
@ -426,7 +370,7 @@ const initPieChart = () => {
},
series: [
{
name: '进度指标',
name: '指标对比',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
@ -442,20 +386,15 @@ const initPieChart = () => {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold',
formatter: function (params) {
// 鼠标悬停时显示当前指标的百分比
return params.value + '%';
}
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: completionRate.value, name: '完成率', itemStyle: { color: '#5470c6' } },
{ value: resolutionRate.value, name: '解决率', itemStyle: { color: '#f56c6c' } },
{ value: timelinessRate.value, name: '及时率', itemStyle: { color: '#67c23a' } }
{ value: completionRate.value, name: '巡检完成率', itemStyle: { color: '#409eff' } },
{ value: timelinessRate.value, name: '解决率', itemStyle: { color: '#67c23a' } }
]
}
]
@ -506,11 +445,7 @@ const fetchDashboardData = async () => {
// 构建查询参数
const queryParams = {
projectId: 1,
type: type,
status: filterStatus.value !== 'all' ? filterStatus.value : undefined,
inspectionType: filterType.value !== 'all' ? filterType.value : undefined,
startTime: dateRange.value.length > 0 ? dateRange.value[0] : undefined,
endTime: dateRange.value.length > 0 ? dateRange.value[1] : undefined
type: type
};
// 调用接口获取数据
@ -526,22 +461,26 @@ const fetchDashboardData = async () => {
solvedProblems.value = data.solvedProblemCount || 0;
avgCompletionTime.value = data.averageCompletionTime ? `${data.averageCompletionTime}分钟` : '0分钟';
// 计算完成率、解决率、及时率
completionRate.value = data.finishInspectionCount && data.finishInspectionCount > 0 ? Math.round(Math.random() * 30 + 60) : 0;
resolutionRate.value = data.solvedProblemCount && data.problemCount ? Math.round((data.solvedProblemCount / data.problemCount) * 100) : 0;
timelinessRate.value = data.finishInspectionCount && data.finishInspectionCount > 0 ? Math.round(Math.random() * 30 + 50) : 0;
// 使用接口返回的xjwcl(巡检完成率)和jjxl(解决效率)
completionRate.value = data.xjwcl ? parseFloat(data.xjwcl) : 0;
timelinessRate.value = data.jjxl ? parseFloat(data.jjxl) : 0;
// 更新问题类型数据
// 由于接口不再返回解决率将其设置为0或保持原值
resolutionRate.value = 0;
// 更新问题类型数据 - 直接使用接口返回的数值,不再计算为百分比
problemTypes.value = {
temperature: data.sbyxzt ? Math.min(100, Math.round(data.sbyxzt * 5)) : 0, // 设备运行状态映射为温度异常
memory: data.ncsyl ? Math.min(100, data.ncsyl * 10) : 0, // 内存使用率
cpu: Math.round(Math.random() * 50 + 20), // CPU负载模拟数据
responseTime: data.xysj ? Math.min(100, data.xysj * 5) : 0, // 响应时间
diskSpace: data.cpsyl ? Math.min(100, data.cpsyl * 8) : 0 // 磁盘使用率
temperature: data.sbyxzt || 0, // 设备运行状态类型问题数量
memory: data.ncsyl || 0, // 内存使用率类型问题数量
cpu: data.fwzt || 0, // 服务状态类型问题数量
responseTime: data.xysj || 0, // 响应时间类型问题数量
diskSpace: data.cpsyl || 0 // 磁盘使用率类型问题数量
};
// 更新饼图
initPieChart();
// 更新柱状图
initBarChart();
} else {
ElMessage.error(response.msg || '获取数据失败');
}
@ -551,17 +490,115 @@ const fetchDashboardData = async () => {
}
};
// 页面加载时获取数据
// 页面加载时直接获取数据
onMounted(() => {
fetchDashboardData();
});
// 初始化柱状图
const initBarChart = () => {
const chartDom = document.getElementById('problemTypesChart');
if (!chartDom) return;
// 销毁旧实例
if (barChart) {
barChart.dispose();
}
// 创建新实例
barChart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params) {
return params[0].name + ': ' + params[0].value + '个';
}
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
top: '5%',
containLabel: true
},
xAxis: {
type: 'value',
name: '问题数量',
axisLabel: {
formatter: '{value}个'
},
splitLine: {
lineStyle: {
type: 'dashed'
}
}
},
yAxis: {
type: 'category',
data: ['温度异常', '内存使用率', 'CPU负载', '响应时间', '磁盘空间'],
axisLabel: {
interval: 0
}
},
series: [
{
name: '问题数量',
type: 'bar',
barWidth: '40%',
data: [
problemTypes.value.temperature,
problemTypes.value.memory,
problemTypes.value.cpu,
problemTypes.value.responseTime,
problemTypes.value.diskSpace
],
itemStyle: {
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: '#5470c6' },
{ offset: 1, color: '#91cc75' }
]),
borderRadius: [0, 4, 4, 0]
},
label: {
show: true,
position: 'right',
formatter: '{c}个'
}
}
]
};
barChart.setOption(option);
// 响应式处理
const handleResize = () => {
if (barChart) {
barChart.resize();
}
};
window.addEventListener('resize', handleResize);
// 组件卸载时移除事件监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
};
// 组件卸载时销毁图表实例
onUnmounted(() => {
if (pieChart) {
pieChart.dispose();
pieChart = null;
}
if (barChart) {
barChart.dispose();
barChart = null;
}
});
// 导航方法
@ -802,6 +839,17 @@ const handleInspectionManagement3 = () => {
margin: 0 auto;
}
/* 柱状图容器 */
.bar-chart-container {
width: 100%;
height: 350px;
margin: 0 auto;
background-color: #fafafa;
border-radius: 8px;
padding: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* 区域标题 */
.section-title {
font-size: 14px;

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="inspection-tasks">
<div class="navigation-tabs">
<!-- <div class="navigation-tabs">
<div class="nav-tab" @click="handleInspection1">待办事项</div>
<div class="nav-tab active" @click="handleInspection2">巡检管理</div>
<div class="nav-tab" @click="handleInspection3">试验管理</div>
@ -9,7 +9,7 @@
<div class="nav-tab" @click="handleInspection5">抢修管理</div>
<div class="nav-tab" @click="handleInspection6">工单管理</div>
<div class="nav-tab" @click="handleInspection7">运维组织</div>
</div>
</div> -->
<!-- 选项卡 -->
<div class="tabs-wrapper">
@ -42,8 +42,8 @@
<el-input v-model="executor" placeholder="执行人"></el-input>
</div>
<div class="filter-actions">
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button type="primary" icon="el-icon-plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
<el-button type="primary" icon="Search" class="search-btn" @click="handleSearch"> 搜索 </el-button>
<el-button type="primary" icon="Plus" class="create-btn" @click="handleCreateTask"> 手动创建任务 </el-button>
</div>
</div>
</div>
@ -133,7 +133,7 @@
</div>
<!-- 添加新任务弹窗 -->
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="500px" :before-close="handleCancelCreateTask">
<el-dialog v-model="createTaskDialogVisible" title="添加新任务" width="750px" :before-close="handleCancelCreateTask">
<el-form ref="createTaskFormRef" :model="createTaskForm" :rules="createTaskRules" label-width="80px">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="createTaskForm.taskName" placeholder="输入任务名称" />
@ -202,7 +202,7 @@
</el-select>
</el-form-item>
<el-form-item label="问题类型" prop="problemType">
<!-- <el-form-item label="问题类型" prop="problemType">
<el-select v-model="createTaskForm.problemType" placeholder="选择问题类型">
<el-option label="磁盘使用率" value="1" />
<el-option label="内存使用率" value="2" />
@ -210,6 +210,27 @@
<el-option label="响应时间" value="4" />
<el-option label="设备运行状态" value="5" />
</el-select>
</el-form-item> -->
<!-- 步骤条 -->
<el-form-item label="执行步骤" class="form-item" style="width: 100%">
<div class="steps-container">
<div class="step-item" v-for="(step, index) in createTaskForm.steps" :key="index">
<div class="step-number">{{ index + 1 }}</div>
<el-input v-model="step.name" placeholder="输入步骤名称" style="flex: 1; margin-right: 10px" />
<el-input v-model="step.intendedPurpose" placeholder="输入预期目的" style="flex: 1; margin-right: 10px" />
<el-date-picker
v-model="step.intendedTime"
type="datetime"
placeholder="选择计划时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
style="width: 180px; margin-right: 10px"
/>
<el-button v-if="createTaskForm.steps.length > 1" type="text" @click="removeStep(index)" style="color: #f56c6c"> 删除 </el-button>
</div>
<el-button type="text" class="add-step-btn" @click="addStep">添加步骤</el-button>
</div>
</el-form-item>
</el-form>
@ -304,7 +325,7 @@
</div>
<div class="info-item">
<span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.person?.phone || '-' }}</span>
<span class="info-value">{{ detailData.person?.phonenumber || '-' }}</span>
</div>
</div>
<div class="info-row">
@ -312,10 +333,6 @@
<span class="info-label">性别</span>
<span class="info-value">{{ detailData.person?.sex === '1' ? '男' : detailData.person?.sex === '2' ? '女' : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">民族</span>
<span class="info-value">{{ detailData.person?.nation || '-' }}</span>
</div>
</div>
</div>
</div>
@ -356,6 +373,26 @@
</div>
</div>
<!-- 执行步骤信息卡片 -->
<div v-if="detailData.nodes && detailData.nodes.length > 0" class="detail-card">
<h3 class="card-title">执行步骤</h3>
<div class="steps-container">
<div v-for="(node, index) in detailData.nodes" :key="node.id || index" class="step-item">
<div class="step-number">{{ node.code || index + 1 }}</div>
<div class="step-info">
<div class="step-name">{{ node.name || '未命名步骤' }}</div>
<div class="step-purpose">{{ node.intendedPurpose || '无说明' }}</div>
<div class="step-time">计划时间{{ formatDateTime(node.intendedTime) }}</div>
<div v-if="node.finishTime" class="step-finish-time">完成时间{{ formatDateTime(node.finishTime) }}</div>
<div v-if="node.remark" class="step-remark">备注{{ node.remark }}</div>
</div>
<div class="step-status" :class="getStatusClass(node.status)">
{{ node.status === '2' ? '未完成' : '已完成' }}
</div>
</div>
</div>
</div>
<!-- 执行结果信息卡片 -->
<div v-if="detailData.taskType === '2' || detailData.taskType === 2" class="detail-card">
<h3 class="card-title">延期信息</h3>
@ -388,37 +425,10 @@
import { ref, computed, onMounted } from 'vue';
import router from '@/router';
import { xjrenwuDetail, xjrenwuExport, xjrenwulist, addxjrenwu, updatexjrenwu, delxjrenwu } from '@/api/zhinengxunjian/xunjian/renwu';
import { xjrenwuDetail, xjrenwulist, addxjrenwu, updatexjrenwu } from '@/api/zhinengxunjian/xunjian/renwu';
import { xunjianUserlist, xunjianlist } from '@/api/zhinengxunjian/xunjian/index';
import { ElMessage, ElLoading } from 'element-plus';
// 根据任务类型获取对应的文本1待执行2已延期3执行中4已完成
const getTaskTypeText = (type) => {
const typeMap = {
'1': '待执行',
'2': '已延期',
'3': '执行中',
'4': '已完成'
};
// 处理可能的数字输入
return typeMap[type.toString()] || '未知类型';
};
// 根据问题类型获取对应的文本1磁盘使用率2内存使用率3服务状态4响应时间5设备运行状态
const getProblemTypeText = (type) => {
const problemTypeMap = {
'1': '磁盘使用率',
'2': '内存使用率',
'3': '服务状态',
'4': '响应时间',
'5': '设备运行状态'
};
// 处理可能的数字输入
return problemTypeMap[type.toString()] || '未知问题';
};
// 激活的选项卡
const activeTab = ref('task');
import { addjiedian } from '@/api/zhinengxunjian/jiedian/index';
import { ElMessage, ElLoading, ElForm } from 'element-plus';
// 筛选条件
const taskStatus = ref('');
@ -428,7 +438,7 @@ const executor = ref('');
// 任务数据 - 初始为空数组通过API获取
const tasks = ref([]);
// 详情弹窗相关变量
// 任务详情弹窗相关变量
const detailDialogVisible = ref(false);
const detailData = ref(null);
const isDetailLoading = ref(false);
@ -459,6 +469,34 @@ const getStatusClass = (status) => {
return statusClassMap[statusStr] || 'status-unknown';
};
// 格式化日期时间
const formatDateTime = (dateTime) => {
if (!dateTime) return '-';
try {
const date = new Date(dateTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (error) {
return dateTime;
}
};
// 获取步骤状态文本
const getStepStatusText = (status) => {
const statusStr = status?.toString() || '';
const statusMap = {
'1': '待执行',
'2': '执行中',
'3': '已完成',
'4': '已延期'
};
return statusMap[statusStr] || '未知状态';
};
// 状态映射配置
const statusConfig = {
pending: {
@ -505,11 +543,10 @@ const getTaskList = async () => {
const params = {
pageSize: pageSize.value,
pageNum: currentPage.value,
personId: executor.value !== '' ? executor.value : undefined,
// 根据任务状态映射到后端需要的taskType
taskType: taskStatus.value ? mapTaskStatusToType(taskStatus.value) : undefined,
// 添加计划类型筛选
planType: planType.value || undefined
personId: 1,
taskType: taskStatus.value || undefined, // 任务状态
planType: planType.value || undefined, // 计划类型
personName: executor.value || undefined // 执行人
};
const response = await xjrenwulist(params);
@ -583,44 +620,6 @@ const getTaskList = async () => {
}
};
// 辅助函数将前端状态映射为后端需要的taskType
const mapTaskStatusToType = (status) => {
const statusMap = {
'pending': '1',
'delayed': '2',
'executing': '3',
'completed': '4'
};
return statusMap[status] || '';
};
// 根据person对象获取执行人姓名
const getExecutorName = (person) => {
if (person && typeof person === 'object' && person.userName) {
return person.userName;
}
const executorMap = {
'zhangming': '张明',
'lihua': '李华',
'wangqiang': '王强',
'zhaowei': '赵伟'
};
return executorMap[person] || '未知用户';
};
// 根据plan对象获取计划名称
const getPlanName = (plan) => {
if (plan && typeof plan === 'object' && plan.planName) {
return plan.planName;
}
const planMap = {
'daily': '每日巡检计划',
'weekly': '每周巡检计划',
'monthly': '每月巡检计划'
};
return planMap[plan] || '未知计划';
};
// 页面加载时获取数据
onMounted(() => {
getTaskList();
@ -675,10 +674,11 @@ const createTaskForm = ref({
timeRange: [],
workTimeRange1: null,
workTimeRange2: null,
relatedPlan: 'all',
relatedPlan: '',
executor: '',
taskType: '1', // 默认待执行
problemType: ''
// problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }] // 任务步骤数组
});
const createTaskRules = {
@ -694,6 +694,17 @@ const handleCreateTask = () => {
openCreateTaskDialog();
};
// 重置步骤表单
const resetStepForm = () => {
Object.keys(stepForm).forEach((key) => {
stepForm[key] = '';
});
currentStep.value = 0;
if (stepFormRef.value) stepFormRef.value.resetFields();
if (deviceFormRef.value) deviceFormRef.value.resetFields();
if (faultFormRef.value) faultFormRef.value.resetFields();
};
// 构建timeInfo字符串
const getTaskTimeInfoString = () => {
const timeInfoArray = [];
@ -719,6 +730,21 @@ const getTaskTimeInfoString = () => {
return timeInfoArray.join(',');
};
// 添加步骤
const addStep = () => {
createTaskForm.value.steps.push({ name: '', intendedPurpose: '', intendedTime: '' });
};
// 删除步骤
const removeStep = (index) => {
// 确保至少保留一个步骤
if (createTaskForm.value.steps.length <= 1) {
ElMessage.warning('至少需要保留一个步骤');
return;
}
createTaskForm.value.steps.splice(index, 1);
};
// 保存任务
const handleSaveTask = async () => {
// 表单验证
@ -727,6 +753,13 @@ const handleSaveTask = async () => {
return;
}
// 验证所有步骤
const hasEmptyStep = createTaskForm.value.steps.some((step) => !step.name.trim() || !step.intendedPurpose.trim());
if (hasEmptyStep) {
ElMessage.warning('请填写完整所有步骤信息');
return;
}
try {
// 获取timeInfo字符串
const taskTimeInfo = getTaskTimeInfoString();
@ -736,13 +769,47 @@ const handleSaveTask = async () => {
return;
}
// 准备步骤数据,与工单列表页面保持一致的格式
const stepsData = createTaskForm.value.steps
.filter((step) => step.name.trim() && step.intendedPurpose.trim())
.map((step, index) => ({
createTime: new Date().toISOString(),
updateTime: new Date().toISOString(),
params: {},
module: 2,
code: index + 1,
name: step.name,
intendedPurpose: step.intendedPurpose,
intendedTime: step.intendedTime ? new Date(step.intendedTime).toISOString() : new Date().toISOString(),
finishTime: '',
remark: '',
status: 2
}));
// 调用添加节点接口,直接传递步骤数组
const jiedianResponse = await addjiedian(stepsData);
if (jiedianResponse.code !== 200) {
ElMessage.error('创建步骤失败');
return;
}
// 获取返回的ids实际返回格式中msg字段包含ids字符串data为null
let nodeIds = '';
if (jiedianResponse.code === 200 && jiedianResponse.msg) {
nodeIds = jiedianResponse.msg;
} else {
ElMessage.warning('未获取到有效的步骤ID');
return;
}
// 构建接口所需的数据结构
const apiData = {
createDept: 0,
createBy: 0,
projectId: 1,
createTime: new Date().toISOString(),
updateBy: 0,
updateTime: new Date().toISOString(),
startTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
params: {
property1: 'string',
property2: 'string'
@ -757,7 +824,8 @@ const handleSaveTask = async () => {
personId: createTaskForm.value.executor !== 'all' ? createTaskForm.value.executor : 0,
taskProgress: 0,
taskType: createTaskForm.value.taskType,
problemType: createTaskForm.value.problemType
// problemType: createTaskForm.value.problemType,
nodeIds: nodeIds // 添加步骤ID字符串与工单列表页面保持一致
};
// 调用新增任务接口
@ -774,10 +842,11 @@ const handleSaveTask = async () => {
timeRange: [],
workTimeRange1: null,
workTimeRange2: null,
relatedPlan: 'all',
relatedPlan: '',
executor: '',
taskType: '1',
problemType: ''
// problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
};
// 重新获取任务列表
getTaskList();
@ -807,21 +876,14 @@ const planList = ref([]);
const getUsersList = async () => {
try {
const response = await xunjianUserlist();
const userRows =
response?.data?.rows && Array.isArray(response.data.rows)
? response.data.rows
: response?.rows && Array.isArray(response.rows)
? response.rows
: Array.isArray(response)
? response
: [];
// 适配新接口格式检查code为200且rows为数组
const userRows = response.code === 200 && response.rows && Array.isArray(response.rows) ? response.rows : [];
userList.value = userRows
.filter((item) => item && typeof item === 'object')
.map((item, index) => ({
label: item.userName || `用户${index + 1}`,
value: item.id || `id_${index}`
.map((item) => ({
label: item.userName || '未知用户',
value: String(item.userId || '') // 使用userId作为唯一标识
}));
if (userList.value.length === 0) {
@ -853,11 +915,8 @@ const getPlansList = async () => {
label: item.planName || `计划${item.id}`,
value: item.id.toString()
}));
planList.value.unshift({ label: '全部计划', value: 'all' });
} catch (error) {
console.error('获取计划列表失败:', error);
planList.value = [{ label: '全部计划', value: 'all' }];
}
};
@ -875,8 +934,13 @@ const handleCancelCreateTask = () => {
taskName: '',
inspectionTarget: '',
timeRange: [],
relatedPlan: 'all',
executor: 'all'
workTimeRange1: null,
workTimeRange2: null,
relatedPlan: '',
executor: '',
taskType: '1',
// problemType: '',
steps: [{ name: '', intendedPurpose: '', intendedTime: '' }]
};
};
@ -981,6 +1045,7 @@ const handleAction = async (task) => {
const updateData = {
...originalTask.rawData,
id: task.id,
startTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
taskType: '3', // 3表示执行中
status: 'executing',
taskProgress: 0
@ -1046,6 +1111,9 @@ const handleAction = async (task) => {
</script>
<style scoped>
@import url('./css/step-bars.css');
@import url('./css/detail-dialog.css');
.inspection-tasks {
padding: 20px;
background-color: #f5f7fa;
@ -1401,6 +1469,96 @@ const handleAction = async (task) => {
overflow-y: auto;
}
/* 步骤条展示样式 */
.step-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
padding: 12px;
background-color: #fafafa;
border-radius: 6px;
transition: all 0.3s ease;
}
.step-item:hover {
background-color: #f5f7fa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.step-number {
width: 28px;
height: 28px;
background-color: #409eff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
font-size: 14px;
font-weight: bold;
flex-shrink: 0;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 500;
color: #1d2129;
margin-bottom: 4px;
font-size: 14px;
}
.step-purpose {
color: #606266;
margin-bottom: 4px;
font-size: 13px;
}
.step-time,
.step-finish-time,
.step-remark {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
.step-status {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
margin-top: 4px;
}
/* 步骤状态样式 */
.step-status.status-pending {
background-color: #e6f7ff;
color: #1677ff;
border: 1px solid #91d5ff;
}
.step-status.status-executing {
background-color: #fffbe6;
color: #fa8c16;
border: 1px solid #ffe58f;
}
.step-status.status-completed {
background-color: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
.step-status.status-delayed {
background-color: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
.detail-card {
margin-bottom: 20px;
padding: 20px;
@ -1580,4 +1738,11 @@ const handleAction = async (task) => {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.step-content {
padding: 30px 20px;
background-color: #fafafa;
border-radius: 8px;
margin-top: 20px;
}
</style>

File diff suppressed because it is too large Load Diff