Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:55:46 +08:00
commit b710247ba7
27 changed files with 6516 additions and 0 deletions

View File

@@ -0,0 +1,758 @@
/**
* 业务服务
* 包含工单服务和报工服务
*/
// ==================== 工单服务 ====================
/**
* 工单服务
* 处理工单相关的API请求
*/
class WorkOrderService {
constructor() {
this.api = apiService;
}
/**
* 获取工单列表
* @param {Object} params - 查询参数
* @param {string} params.search - 搜索关键词(工单编号模糊查询)
* @param {string} params.exactWorkOrderCode - 工单编号精确查询
* @param {number} params.page - 页码
* @param {number} params.pageSize - 每页数量
* @param {Array<number>} params.workOrderStatusList - 工单业务状态列表
* @param {number} params.pauseFlag - 是否暂停 0未暂停 1已暂停
* @returns {Promise<Object>}
*/
async getWorkOrderList(params = {}) {
try {
console.log('从服务器获取工单列表', params);
// 构建请求体根据API文档格式
const requestBody = {
page: params.page || 1,
size: params.pageSize || 50
};
// 如果有搜索关键词,使用模糊查询
if (params.search) {
requestBody.workOrderCode = params.search.trim();
}
// 如果有精确查询
if (params.exactWorkOrderCode) {
requestBody.exactWorkOrderCode = params.exactWorkOrderCode.trim();
}
// 工单状态筛选
if (params.workOrderStatusList && Array.isArray(params.workOrderStatusList)) {
requestBody.workOrderStatusList = params.workOrderStatusList;
}
// 暂停状态筛选
if (params.pauseFlag !== undefined) {
requestBody.pauseFlag = params.pauseFlag;
}
// 调用API接口POST方法
const response = await this.api.post('/openapi/domain/web/v1/route/med/open/v2/work_order/base/_list', requestBody);
// 处理响应数据
const result = this.processWorkOrderListResponse(response);
return result;
} catch (error) {
console.error('获取工单列表失败:', error);
// 如果是网络错误,返回模拟数据
if (error.name === 'TypeError' || error.message.includes('Failed to fetch')) {
console.log('网络错误,返回模拟工单数据');
return this.getMockWorkOrderList(params);
}
throw error;
}
}
/**
* 处理工单列表响应数据
* @param {Object} response - API响应
* @returns {Object}
*/
processWorkOrderListResponse(response) {
// 根据API文档响应格式{ code, message, data: { list, page, total } }
if (response.code === 200 || response.code === 0) {
const data = response.data;
if (!data || !data.list) {
throw new Error('响应数据格式错误缺少data或list字段');
}
return {
workOrders: data.list.map(item => {
// 提取物料信息
const materialInfo = item.materialInfo || {};
const materialBaseInfo = materialInfo.baseInfo || {};
// 提取数量信息BaseAmountDisplay对象
const qualifiedAmount = item.qualifiedHoldAmount || {};
const disqualifiedAmount = item.disqualifiedHoldAmount || {};
const totalAmount = item.totalHoldAmount || {};
return {
id: item.workOrderCode || item.workOrderId?.toString() || '',
workOrderId: item.workOrderId,
workOrderCode: item.workOrderCode,
name: materialBaseInfo.name || materialInfo.name || item.workOrderCode,
status: item.workOrderStatus?.code || 'active',
// 物料信息(保留完整结构)
materialInfo: item.materialInfo, // 保留完整的materialInfo对象
materialId: materialBaseInfo.id || materialInfo.id,
materialName: materialBaseInfo.name || materialInfo.name,
materialCode: materialBaseInfo.code || materialInfo.code,
// 数量信息
qualifiedQuantity: qualifiedAmount.amount || 0,
qualifiedQuantityDisplay: qualifiedAmount.amountDisplay || '0',
disqualifiedQuantity: disqualifiedAmount.amount || 0,
disqualifiedQuantityDisplay: disqualifiedAmount.amountDisplay || '0',
totalQuantity: totalAmount.amount || 0,
totalQuantityDisplay: totalAmount.amountDisplay || '0',
// 时间信息
createdAt: item.createdAt,
updatedAt: item.updatedAt,
plannedStartTime: item.plannedStartTime,
plannedEndTime: item.plannedEndTime,
// 其他信息
pauseFlag: item.pauseFlag,
workOrderStatus: item.workOrderStatus,
workOrderType: item.workOrderType
};
}),
total: data.total || 0,
page: data.page || 1,
pageSize: data.size || 50
};
} else {
throw new Error(response.message || `获取工单列表失败 (code: ${response.code})`);
}
}
/**
* 获取模拟工单列表(用于离线模式或测试)
* @param {Object} params - 查询参数
* @returns {Object}
*/
getMockWorkOrderList(params = {}) {
const mockData = [
{
id: 'cyy-SOP',
name: '测试工单-SOP',
status: 'active',
materialId: 'M001',
materialName: '物料1-工序a-10',
materialCode: 'wuliao1-gongxua-10'
},
{
id: 'cyy-chengliangrenwu',
name: '测试工单-成量任务',
status: 'active',
materialId: 'M002',
materialName: '物料2-工序b-20',
materialCode: 'wuliao2-gongxub-20'
},
{
id: 'gd2510300131-',
name: '工单2510300131',
status: 'active',
materialId: 'M003',
materialName: '物料3-工序c-30',
materialCode: 'wuliao3-gongxuc-30'
},
{
id: 'gd2510270130-',
name: '工单2510270130',
status: 'active',
materialId: 'M004',
materialName: '物料4-工序d-40',
materialCode: 'wuliao4-gongxud-40'
},
{
id: 'gd2510210129-',
name: '工单2510210129',
status: 'active',
materialId: 'M005',
materialName: '物料5-工序e-50',
materialCode: 'wuliao5-gongxue-50'
},
{
id: 'gd2509020128-',
name: '工单2509020128',
status: 'active',
materialId: 'M006',
materialName: '物料6-工序f-60',
materialCode: 'wuliao6-gongxuf-60'
},
{
id: 'gd2509010126-',
name: '工单2509010126',
status: 'active',
materialId: 'M007',
materialName: '物料7-工序g-70',
materialCode: 'wuliao7-gongxug-70'
},
{
id: 'gd2509010125-',
name: '工单2509010125',
status: 'active',
materialId: 'M008',
materialName: '物料8-工序h-80',
materialCode: 'wuliao8-gongxuh-80'
},
{
id: 'gd2508260124-',
name: '工单2508260124',
status: 'active',
materialId: 'M009',
materialName: '物料9-工序i-90',
materialCode: 'wuliao9-gongxui-90'
},
{
id: 'gd2507210123-',
name: '工单2507210123',
status: 'active',
materialId: 'M010',
materialName: '物料10-工序j-100',
materialCode: 'wuliao10-gongxuj-100'
}
];
// 应用搜索过滤
let filteredData = mockData;
if (params.search) {
const searchTerm = params.search.toLowerCase();
filteredData = mockData.filter(item =>
item.id.toLowerCase().includes(searchTerm) ||
item.name.toLowerCase().includes(searchTerm)
);
}
return {
workOrders: filteredData,
total: filteredData.length,
page: params.page || 1,
pageSize: params.pageSize || 50
};
}
/**
* 预加载工单数据
*/
async preloadWorkOrders() {
try {
console.log('预加载工单数据...');
await this.getWorkOrderList({ pageSize: 100 });
} catch (error) {
console.error('预加载工单数据失败:', error);
}
}
}
// 创建全局工单服务实例
const workOrderService = new WorkOrderService();
// ==================== 报工服务 ====================
/**
* 报工服务
* 处理报工相关的API请求
*/
class ReportService {
constructor() {
this.api = apiService;
}
/**
* 获取报工必填参数
* 从接口获取报工所需的必填参数如taskId、progressReportMaterial等
* @param {Object} params - 查询参数
* @param {string|number} params.workOrderId - 工单ID
* @param {string|number} params.materialId - 物料ID
* @returns {Promise<Object>} 返回必填参数对象
*/
async getReportRequiredParams(params) {
try {
console.log('获取报工必填参数:', params);
const { workOrderId, materialId } = params;
if (!workOrderId ) {
throw new Error('工单ID获取失败');
}
const taskListRequest = {
page: 1,
size: 10,
workOrderIdList: [workOrderId]
};
const taskListResponse = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/produce_task/_list', taskListRequest);
if (taskListResponse.code !== 200 || !taskListResponse.data || !taskListResponse.data.list || taskListResponse.data.list.length === 0) {
throw new Error('未找到对应的生产任务');
}
// 取第一个任务(通常一个工单对应一个主任务)
const task = taskListResponse.data.list[0];
const taskId = task.taskId;
const executorIds = (task.executorList || []).map(item => item.id); // 只取执行人id数组
if (!taskId) {
throw new Error('生产任务中未找到任务ID');
}
// 2. 通过生产任务ID获取报工物料列表
console.log('获取报工物料列表...');
const materialListRequest = {
taskId: taskId
};
const materialListResponse = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list_progress_report_materials', materialListRequest);
if (materialListResponse.code !== 200 || !materialListResponse.data || !materialListResponse.data.outputMaterials || materialListResponse.data.outputMaterials.length === 0) {
throw new Error('未找到报工物料信息');
}
// 获取报工物料列表
const outputMaterials = materialListResponse.data.outputMaterials;
// 优先选择主产出物料mainFlag为true否则选择第一个
let selectedMaterial = outputMaterials.find(m => m.mainFlag === true) || outputMaterials[0];
if (!selectedMaterial) {
throw new Error('未找到可报工的物料');
}
// 从报工物料中提取报工关系信息
const progressReportKey = selectedMaterial.progressReportKey;
if (!progressReportKey) {
throw new Error('报工物料中未找到报工关系信息');
}
// 3. 构建progressReportMaterial对象
const progressReportMaterial = {
lineId: progressReportKey.lineId,
materialId: progressReportKey.materialId || selectedMaterial.materialInfo?.baseInfo?.id || materialId,
reportProcessId: progressReportKey.reportProcessId
};
// 验证必填字段
if (!progressReportMaterial.lineId) {
throw new Error('无法获取物料行ID');
}
if (!progressReportMaterial.materialId) {
throw new Error('无法获取物料ID');
}
if (!progressReportMaterial.reportProcessId) {
throw new Error('无法获取报工工序ID');
}
// 4. 返回必填参数
const requiredParams = {
taskId: taskId,
progressReportMaterial: progressReportMaterial,
// 额外信息,可能用于构建报工参数
materialInfo: selectedMaterial.materialInfo,
outputMaterialUnit: selectedMaterial.outputMaterialUnit, // 报工单位信息
lineId: progressReportMaterial.lineId,
reportProcessId: progressReportMaterial.reportProcessId,
executorIds: executorIds, // 执行人列表
// 报工物料的其他信息
reportType: selectedMaterial.reportType || [], // 可报工方式
mainFlag: selectedMaterial.mainFlag, // 是否为主产出
warehousingFlag: selectedMaterial.warehousingFlag, // 是否入库
autoWarehousingFlag: selectedMaterial.autoWarehousingFlag // 是否自动入库
};
console.log('成功获取报工必填参数:', requiredParams);
return requiredParams;
} catch (error) {
console.error('获取报工必填参数失败:', error);
throw error;
}
}
/**
* 构建报工请求参数
* 将必填参数和表单数据合并,构建完整的报工请求参数
* @param {Object} requiredParams - 从接口获取的必填参数
* @param {Object} formData - 表单数据
* @param {Object} formData.workOrderId - 工单ID
* @param {Object} formData.materialId - 物料ID
* @param {number} formData.quantity - 报工数量
* @param {number} formData.auxiliaryQuantity - 辅助数量
* @param {number} formData.qcStatus - 质量状态1合格 2让步合格 3代检 4不合格
* @param {number} formData.reportType - 报工方式1扫码报工-合格 2记录报工-合格 3扫码报工-不合格 4记录报工-不合格 5打码报工-合格 6打码报工-不合格)
* @param {Array<number>} formData.executorIds - 执行人ID列表
* @returns {Object} 完整的报工请求参数
* @note storageLocationId 已预置为 1716848012872791
*/
buildReportRequestParams(requiredParams, formData) {
// 从必填参数中获取基础信息
const {
taskId, // 生产任务ID必填
progressReportMaterial, // 报工物料对象(必填)
reportProcessId, // 报工工序ID
lineId, // 物料行ID
outputMaterialUnit, // 报工单位信息
executorIds // 执行人ID列表
} = requiredParams;
// 获取报工单位ID从outputMaterialUnit中获取
const reportUnitId = outputMaterialUnit?.id;
if (!reportUnitId) {
throw new Error('无法获取报工单位ID');
}
// 构建报工请求参数
const requestParams = {
// 必填字段
taskId: taskId,
progressReportMaterial: progressReportMaterial || {
materialId: formData.materialId,
lineId: lineId,
reportProcessId: reportProcessId
},
qcStatus: formData.qcStatus || 1, // 默认合格
reportType: formData.reportType || 2, // 默认记录报工-合格
// 报工详情(必填)
progressReportItems: [{
executorIds: executorIds ,
progressReportMaterialItems: [{
reportAmount: formData.quantity,
reportUnitId: reportUnitId, // 必填报工单位ID
remark: formData.remark || undefined,
// auxAmount1: formData.auxiliaryQuantity || undefined,
// auxUnitId1: formData.auxUnitId1 || undefined
}]
}],
// 可选字段
storageLocationId: 1716848012872791,// 预置仓位 Id不可修改
reportStartTime: formData.reportStartTime ? new Date(formData.reportStartTime).getTime() : undefined,
reportEndTime: formData.reportEndTime ? new Date(formData.reportEndTime).getTime() : undefined,
actualExecutorIds: executorIds || [],
actualEquipmentIds: formData.equipmentIds || [],
workHour: formData.workHour,
workHourUnit: formData.workHourUnit ,
qcDefectReasonIds: formData.qcDefectReasonIds || [] // 不良原因(质量状态为不合格时)
};
// 移除undefined字段
Object.keys(requestParams).forEach(key => {
if (requestParams[key] === undefined) {
delete requestParams[key];
}
});
// 递归移除嵌套对象中的undefined字段
const cleanUndefined = (obj) => {
if (Array.isArray(obj)) {
return obj.map(item => cleanUndefined(item));
} else if (obj && typeof obj === 'object') {
const cleaned = {};
Object.keys(obj).forEach(key => {
if (obj[key] !== undefined) {
cleaned[key] = cleanUndefined(obj[key]);
}
});
return cleaned;
}
return obj;
};
return cleanUndefined(requestParams);
}
/**
* 提交报工
* @param {Object} reportData - 报工数据
* @param {string|number} reportData.workOrderId - 工单ID
* @param {string|number} reportData.workOrderCode - 工单编号
* @param {string|number} reportData.materialId - 物料ID
* @param {number} reportData.quantity - 数量
* @param {number} reportData.auxiliaryQuantity - 辅助数量
* @param {number} reportData.qcStatus - 质量状态1合格 2让步合格 3代检 4不合格
* @param {number} reportData.reportType - 报工方式1-6
* @param {Array<number>} reportData.executorIds - 执行人ID列表
* @param {number} reportData.reportUnitId - 报工单位ID
* @param {number} reportData.auxUnitId1 - 辅助单位1 ID
* @returns {Promise<Object>}
*/
async submitReport(reportData) {
try {
// 验证报工数据
this.validateReportData(reportData);
// 1. 获取报工必填参数
const requiredParams = await this.getReportRequiredParams({
workOrderId: reportData.workOrderId,
materialId: reportData.materialId
});
// 2. 构建完整的报工请求参数
const requestParams = this.buildReportRequestParams(requiredParams, reportData);
console.log('提交报工请求参数:', requestParams);
// 3. 调用批量报工接口
const response = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_progress_report', requestParams);
// 4. 处理响应
const result = this.processReportResponse(response);
console.log('报工提交成功:', result);
return result;
} catch (error) {
console.error('报工提交失败:', error);
showToast(error.message || '报工提交失败', 'error');
throw error;
}
}
/**
* 验证报工数据
* @param {Object} data - 报工数据
*/
validateReportData(data) {
const errors = [];
if (!data.workOrderId) {
errors.push('工单ID不能为空');
}
if (!data.quantity || data.quantity <= 0) {
errors.push('数量必须大于0');
}
if (!data.auxiliaryQuantity || data.auxiliaryQuantity <= 0) {
errors.push('辅助数量必须大于0');
}
if (errors.length > 0) {
throw new Error(errors.join(', '));
}
}
/**
* 处理报工响应
* 根据批量报工接口文档,响应格式:{ code, message, data: { messageTraceId, progressReportRecordIds } }
* @param {Object} response - API响应
* @returns {Object}
*/
processReportResponse(response) {
// 根据批量报工接口文档,响应格式:{ code, message, data: { messageTraceId, progressReportRecordIds } }
if (response.code === 200 || response.code === 0) {
const data = response.data || {};
return {
success: true,
messageTraceId: data.messageTraceId,
progressReportRecordIds: data.progressReportRecordIds || [],
message: response.message || '报工提交成功',
reportTime: new Date().toISOString()
};
} else {
throw new Error(response.message || `报工提交失败 (code: ${response.code})`);
}
}
/**
* 根据生产任务查询报工记录列表
* @param {Object} params - 查询参数
* @param {number|Array<number>} params.taskId - 生产任务ID或ID列表可选
* @param {number|Array<number>} params.workOrderId - 工单ID或ID列表可选
* @param {number} params.reportTimeFrom - 报工时间From(闭区间),时间戳(可选)
* @param {number} params.reportTimeTo - 报工时间To(开区间),时间戳(可选)
* @param {number} params.page - 请求页默认1可选
* @param {number} params.size - 每页大小默认200可选
* @param {Array<number>} params.processIdList - 工序ID列表可选
* @param {Array<number>} params.executorIdList - 可执行人ID列表可选
* @param {Array<number>} params.qcStatusList - 质量状态列表(可选)
* @param {Array<Object>} params.sorter - 排序条件列表(可选),列表顺序表示排序顺序
* @param {string} params.sorter[].field - 排序字段,如 "reportTime", "taskCode" 等
* @param {string} params.sorter[].order - 排序规律,默认 "asc""asc" 升序 "desc" 降序
* @returns {Promise<Object>} 返回报工记录列表(分页响应)
* @returns {boolean} returns.success - 是否成功
* @returns {Array<Object>} returns.records - 报工记录数组,每个记录包含以下字段:
* @returns {number} returns.records[].id - 报工记录详情id必填
* @returns {number} returns.records[].lineId - 物料行id必填
* @returns {number} returns.records[].reportRecordId - 报工记录id必填
* @returns {number} returns.records[].taskId - 生产任务Id必填
* @returns {string} returns.records[].taskCode - 生产任务编号(必填)
* @returns {number} returns.records[].workOrderId - 工单Id必填
* @returns {string} returns.records[].workOrderCode - 工单编号(必填)
* @returns {number} returns.records[].processId - 工序Id必填
* @returns {string} returns.records[].processCode - 工序编号(必填)
* @returns {string} returns.records[].processName - 工序名称(必填)
* @returns {number} returns.records[].mainMaterialId - 工单物料id必填
* @returns {string} returns.records[].mainMaterialCode - 工单物料编号(必填)
* @returns {string} returns.records[].mainMaterialName - 工单物料名称(必填)
* @returns {Object} returns.records[].materialInfo - 报工物料信息(必填),包含 baseInfo物料基本信息、attribute物料属性信息
* @returns {Object} returns.records[].reportBaseAmount - 报工数量1对应数量对象必填包含 amount数量和 unit单位信息
* @returns {Object} returns.records[].reportBaseAmountDisplay - 报工数量1对应数量对象-无科学技术法(必填),包含 amount、amountDisplay、unitCode、unitId、unitName
* @returns {number} returns.records[].reportTime - 报工时间,时间戳(必填)
* @returns {Object} returns.records[].reporter - 报工人员(必填),包含 id、code、name、username、avatarUrl
* @returns {Array<Object>} returns.records[].producers - 生产人员(必填),数组元素包含 id、code、name、username、avatarUrl
* @returns {Object} returns.records[].qcStatus - 质量状态(必填),包含 code枚举code和 message枚举信息
* @returns {Object} returns.records[].workHourUnit - 工时单位(必填),包含 code 和 message
* @returns {number} returns.records[].startTime - 报工开始时间,时间戳(可选)
* @returns {number} returns.records[].endTime - 报工结束时间,时间戳(可选)
* @returns {number} returns.records[].workHour - 工时(可选)
* @returns {string} returns.records[].batchNo - 批次号(可选)
* @returns {number} returns.records[].batchNoId - 批次号Id可选
* @returns {string} returns.records[].flowCardCode - 流转卡编号(可选)
* @returns {string} returns.records[].qrCode - 标示码(可选)
* @returns {string} returns.records[].reportRecordCode - 报工记录编号(可选)
* @returns {string} returns.records[].remark - 备注(可选)
* @returns {number} returns.records[].autoWarehousingFlag - 是否自动入库标识 0-否 1-是(可选)
* @returns {Array<Object>} returns.records[].equipments - 设备列表(可选),数组元素包含 id、code、name
* @returns {Array<Object>} returns.records[].executors - 计划执行人(可选),数组元素包含 id、code、name、username、avatarUrl
* @returns {Array<Object>} returns.records[].produceDepartments - 生产部门列表(可选),数组元素包含 id、code、name
* @returns {Array<Object>} returns.records[].customFields - 自定义字段(可选),数组元素包含 fieldCode、fieldName、fieldValue 等
* @returns {Array<Object>} returns.records[].qcDefectReasons - 不良原因(可选),数组元素包含 id、name
* @returns {Array<string>} returns.records[].salesOrderCode - 订单编号(可选)
* @returns {Array<number>} returns.records[].pictureIds - 图片ids可选
* @returns {Object} returns.records[].reportAuxBaseAmount1 - 报工数量2对应数量对象可选包含 amount 和 unit
* @returns {number} returns.records[].updatedAt - 更新时间,时间戳(可选)
* @returns {number} returns.total - 总条数
* @returns {number} returns.page - 当前页
* @returns {number} returns.size - 每页大小
* @returns {string} returns.message - 返回消息
*/
async getReportRecordsByTask(params) {
try {
const {
taskId,
workOrderId,
reportTimeFrom,
reportTimeTo,
page = 1,
size = 200,
processIdList,
executorIdList,
qcStatusList,
sorter
} = params;
// 构建请求参数
const requestParams = {
page: parseInt(page),
size: parseInt(size)
};
// 添加排序参数(如果未提供,默认按报工时间降序)
if (sorter && Array.isArray(sorter) && sorter.length > 0) {
// 使用用户提供的排序条件
requestParams.sorter = sorter.map(item => ({
field: item.field,
order: item.order
}));
} else {
// 默认按报工时间降序排序
requestParams.sorter = [{
field: 'reportTime',
order: 'desc'
}];
}
// 添加任务ID列表支持单个或数组
if (taskId) {
requestParams.taskIds = Array.isArray(taskId) ? taskId.map(id => parseInt(id)) : [parseInt(taskId)];
}
// 添加工单ID列表支持单个或数组
if (workOrderId) {
requestParams.workOrderIdList = Array.isArray(workOrderId)
? workOrderId.map(id => parseInt(id))
: [parseInt(workOrderId)];
}
// 添加报工时间范围
if (reportTimeFrom) {
requestParams.reportTimeFrom = parseInt(reportTimeFrom);
}
if (reportTimeTo) {
requestParams.reportTimeTo = parseInt(reportTimeTo);
}
// 添加其他可选参数
if (processIdList && processIdList.length > 0) {
requestParams.processIdList = processIdList.map(id => parseInt(id));
}
if (executorIdList && executorIdList.length > 0) {
requestParams.executorIdList = executorIdList.map(id => parseInt(id));
}
if (qcStatusList && qcStatusList.length > 0) {
requestParams.qcStatusList = qcStatusList.map(status => parseInt(status));
}
// 验证至少有一个查询条件
if (!requestParams.taskIds && !requestParams.workOrderIdList) {
throw new Error('必须提供 taskId 或 workOrderId 中的至少一个');
}
console.log('查询报工记录,参数:', requestParams);
// 调用报工记录列表接口
const response = await this.api.post('/openapi/domain/web/v1/route/mfg/open/v1/progress_report/_list', requestParams);
// 处理响应
if (response.code !== 200 && response.code !== 0) {
throw new Error(response.message || `查询报工记录失败 (code: ${response.code})`);
}
// 响应格式:{ code, message, data: { list, page, total } }
// data.list 是报工记录数组,每个记录包含以下主要字段:
// - 必填字段id, lineId, reportRecordId, taskId, taskCode, workOrderId, workOrderCode,
// processId, processCode, processName, mainMaterialId, mainMaterialCode, mainMaterialName,
// materialInfo, reportBaseAmount, reportBaseAmountDisplay, reportTime, reporter, producers, qcStatus, workHourUnit
// - 可选字段startTime, endTime, workHour, batchNo, flowCardCode, qrCode, remark,
// equipments, executors, customFields, qcDefectReasons 等
const data = response.data || {};
const reportRecords = data.list || [];
const total = data.total || 0;
const currentPage = data.page || page;
console.log(`查询到 ${reportRecords.length} 条报工记录,共 ${total}`);
return {
success: true,
records: reportRecords, // 报工记录数组,详细字段说明见函数 JSDoc 注释
total: total,
page: currentPage,
size: size,
message: response.message || '查询成功'
};
} catch (error) {
console.error('查询报工记录失败:', error);
throw error;
}
}
}
// 创建全局报工服务实例
const reportService = new ReportService();

View File

@@ -0,0 +1,781 @@
/**
* 核心服务
* 包含API服务和认证服务
*/
// ==================== API服务 ====================
/**
* API服务基础类
* 封装HTTP请求自动处理认证和错误
*/
class ApiService {
constructor() {
this.baseURL = this.getBaseURL();
this.defaultHeaders = {
'Content-Type': 'application/json'
};
}
/**
* 获取API基础URL
* @returns {string}
*/
getBaseURL() {
// 根据环境自动判断API地址
const hostname = window.location.hostname;
if (hostname === 'localhost' || hostname === '127.0.0.1') {
// 开发环境使用黑湖API服务器
return 'https://v3-feature.blacklake.cn/api';
} else {
// 生产环境使用黑湖API服务器
return 'https://v3-feature.blacklake.cn/api';
}
}
/**
* 构建完整的请求URL
* @param {string} endpoint - API端点
* @returns {string}
*/
buildURL(endpoint) {
// 如果endpoint已经是完整URL直接返回
if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) {
return endpoint;
}
// 确保endpoint以/开头
if (!endpoint.startsWith('/')) {
endpoint = '/' + endpoint;
}
return this.baseURL + endpoint;
}
/**
* 获取请求头
* @param {Object} customHeaders - 自定义请求头
* @returns {Object}
*/
getHeaders(customHeaders = {}) {
const headers = { ...this.defaultHeaders, ...customHeaders };
// 添加认证头
if (authService && authService.isAuthenticated()) {
Object.assign(headers, authService.getAuthHeaders());
}
return headers;
}
/**
* 处理响应
* @param {Response} response - fetch响应对象
* @returns {Promise<any>}
*/
async handleResponse(response) {
// 解析响应数据
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
// 处理HTTP状态码错误
if (!response.ok) {
let errorMessage = `请求失败: ${response.status}`;
if (typeof data === 'object' && data.message) {
errorMessage = data.message;
} else if (typeof data === 'string') {
errorMessage = data;
}
// 处理认证错误
if (response.status === 401 || response.status === 403) {
if (authService) {
const canRetry = await authService.handleAuthError(response);
if (canRetry) {
throw new Error('AUTH_RETRY'); // 特殊错误,表示可以重试
}
}
}
throw new Error(errorMessage);
}
// 检查业务错误码即使HTTP状态码是200业务也可能返回错误
if (typeof data === 'object' && data.code !== undefined) {
// 业务错误码非200且非0表示错误
if (data.code !== 200 && data.code !== 0) {
// 处理认证相关的业务错误
if (data.subCode && (
data.subCode.includes('TOKEN') ||
data.subCode.includes('AUTH') ||
data.subCode.includes('APP_KEY') ||
data.subCode.includes('NOT_EXIST')
)) {
console.warn('业务认证错误:', data.subCode, data.message);
if (authService) {
const canRetry = await authService.handleAuthError(response);
if (canRetry) {
throw new Error('AUTH_RETRY');
}
}
}
throw new Error(data.message || `业务错误 (code: ${data.code}, subCode: ${data.subCode})`);
}
}
return data;
}
/**
* 发送HTTP请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async request(endpoint, options = {}) {
const url = this.buildURL(endpoint);
const config = {
method: 'GET',
headers: this.getHeaders(options.headers),
...options
};
// 如果有body且不是FormData转换为JSON字符串
if (config.body && !(config.body instanceof FormData)) {
if (typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
}
}
let retryCount = 0;
const maxRetries = 2;
while (retryCount <= maxRetries) {
try {
console.log(`API请求: ${config.method} ${url}`, config.body ? JSON.parse(config.body) : '');
const response = await fetch(url, config);
const result = await this.handleResponse(response);
console.log(`API响应: ${config.method} ${url}`, result);
return result;
} catch (error) {
console.error(`API错误: ${config.method} ${url}`, error);
// 如果是认证错误且可以重试
if (error.message === 'AUTH_RETRY' && retryCount < maxRetries) {
retryCount++;
// 更新请求头中的认证信息
config.headers = this.getHeaders(options.headers);
continue;
}
// 网络错误重试
if (error.name === 'TypeError' && error.message.includes('Failed to fetch') && retryCount < maxRetries) {
retryCount++;
await this.delay(1000 * retryCount); // 延迟重试
continue;
}
throw error;
}
}
}
/**
* GET请求
* @param {string} endpoint - API端点
* @param {Object} params - 查询参数
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async get(endpoint, params = {}, options = {}) {
// 构建查询字符串
const queryString = new URLSearchParams(params).toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, {
method: 'GET',
...options
});
}
/**
* POST请求
* @param {string} endpoint - API端点
* @param {any} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async post(endpoint, data = null, options = {}) {
return this.request(endpoint, {
method: 'POST',
body: data,
...options
});
}
/**
* PUT请求
* @param {string} endpoint - API端点
* @param {any} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async put(endpoint, data = null, options = {}) {
return this.request(endpoint, {
method: 'PUT',
body: data,
...options
});
}
/**
* DELETE请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise<any>}
*/
async delete(endpoint, options = {}) {
return this.request(endpoint, {
method: 'DELETE',
...options
});
}
/**
* 上传文件
* @param {string} endpoint - API端点
* @param {File|FileList} files - 文件对象
* @param {Object} additionalData - 额外数据
* @param {Function} onProgress - 进度回调
* @returns {Promise<any>}
*/
async upload(endpoint, files, additionalData = {}, onProgress = null) {
const formData = new FormData();
// 添加文件
if (files instanceof FileList) {
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
} else if (files instanceof File) {
formData.append('file', files);
}
// 添加额外数据
for (const key in additionalData) {
formData.append(key, additionalData[key]);
}
const options = {
method: 'POST',
body: formData,
headers: {} // 不设置Content-Type让浏览器自动设置
};
// 如果需要进度回调使用XMLHttpRequest
if (onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const result = JSON.parse(xhr.responseText);
resolve(result);
} catch (e) {
resolve(xhr.responseText);
}
} else {
reject(new Error(`上传失败: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('上传失败'));
});
xhr.open('POST', this.buildURL(endpoint));
// 添加认证头
const headers = this.getHeaders();
for (const key in headers) {
if (key !== 'Content-Type') { // 不设置Content-Type
xhr.setRequestHeader(key, headers[key]);
}
}
xhr.send(formData);
});
}
return this.request(endpoint, options);
}
/**
* 延迟函数
* @param {number} ms - 延迟毫秒数
* @returns {Promise}
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 批量请求
* @param {Array} requests - 请求数组,每个元素包含 {method, endpoint, data}
* @returns {Promise<Array>}
*/
async batch(requests) {
const promises = requests.map(req => {
const { method = 'GET', endpoint, data, options = {} } = req;
switch (method.toUpperCase()) {
case 'GET':
return this.get(endpoint, data, options);
case 'POST':
return this.post(endpoint, data, options);
case 'PUT':
return this.put(endpoint, data, options);
case 'DELETE':
return this.delete(endpoint, options);
default:
return Promise.reject(new Error(`不支持的请求方法: ${method}`));
}
});
return Promise.allSettled(promises);
}
/**
* 设置基础URL
* @param {string} baseURL - 新的基础URL
*/
setBaseURL(baseURL) {
this.baseURL = baseURL;
}
/**
* 设置默认请求头
* @param {Object} headers - 请求头对象
*/
setDefaultHeaders(headers) {
this.defaultHeaders = { ...this.defaultHeaders, ...headers };
}
}
// 创建全局API服务实例
const apiService = new ApiService();
// ==================== 认证服务 ====================
/**
* 默认配置
* 修改登录账号时,需要同时更新 appKey
*/
const AUTH_CONFIG = {
// 登录信息
login: {
type: 1,
username: "cyy",
code: "67768820",
password: "794db7135639d5b59cd7e53b325f36a8f24fb906e95e403c49e89b99046654fae36a940c1e8496b75b9e69d4f79022c9123aa0d8cd665e2ea9cf584242a702664e77fdd2399c452bd03a174a3edbb41b86a406851b4da8b11b8faa7044925e3e9bffd4fd5afb14c70f592a2114ce5f45cf567e2e1f0d8688ef345ca28c2687c5"
},
// AppKey用于获取user-access token
appKey: "cli_1764100796489835"
};
/**
* 认证服务
* 处理token获取、存储和管理
*/
class AuthService {
constructor() {
this.token = null; // 最终用于业务接口的user-access token
this.tokenKey = 'work_report_user_access_token';
this.init();
}
/**
* 初始化认证服务
*/
async init() {
try {
// 1. 先查看URL里是否存在code
const code = getUrlParameter('code');
if (code) {
// 如果存在code直接使用code获取user-access token
console.log('从URL参数获取到code:', code);
await this.getUserAccessTokenByCode(code);
} else {
// 如果不存在code走登录流程
console.log('URL参数中没有code走登录流程...');
// 2.1 先登录获取token
console.log('开始登录...');
const loginResult = await this.loginAndGetToken();
if (!loginResult || !loginResult.token) {
throw new Error('登录失败未获取到token');
}
console.log('登录成功获取到登录token');
// 2.2 使用登录token获取code
console.log('使用登录token获取code...');
const codeFromApi = await this.getCodeByLoginToken(loginResult.token);
if (!codeFromApi) {
throw new Error('获取code失败');
}
console.log('成功获取code:', codeFromApi);
// 2.3 使用code获取user-access token
await this.getUserAccessTokenByCode(codeFromApi);
}
console.log('认证初始化完成user-access token已获取');
} catch (error) {
console.error('认证初始化失败:', error);
showToast('认证初始化失败,请刷新页面重试', 'error');
throw error; // 抛出错误,让调用方知道初始化失败
}
}
/**
* 使用code获取user-access token
* @param {string} code - code参数
*/
async getUserAccessTokenByCode(code) {
try {
if (!code) {
throw new Error('code参数不能为空');
}
console.log('使用code获取user-access token...');
const requestBody = {
code: code,
appKey: AUTH_CONFIG.appKey
};
const response = await fetch('https://v3-feature.blacklake.cn/api/openapi/domain/api/v1/access_token/_get_user_token_for_customized', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`获取user-access token失败: ${response.status}`);
}
const data = await response.json();
console.log('获取user-access token响应:', data);
// 根据实际响应格式提取token
if (data.code === 200 || data.code === 0) {
const tokenData = data.data || data.result || data;
const userAccessToken = tokenData.token ||
tokenData.userAccessToken ||
tokenData.accessToken ||
data.token ||
data.userAccessToken ||
data.accessToken;
if (userAccessToken) {
this.token = userAccessToken;
this.saveToken(userAccessToken);
console.log('成功获取user-access token');
} else {
throw new Error('响应中没有找到user-access token');
}
} else {
throw new Error(data.message || `获取user-access token失败 (code: ${data.code})`);
}
} catch (error) {
console.error('获取user-access token失败:', error);
throw error;
}
}
/**
* 使用登录token获取code
* @param {string} loginToken - 登录获取的token
* @returns {Promise<string>} 返回code
*/
async getCodeByLoginToken(loginToken) {
try {
if (!loginToken) {
throw new Error('登录token不能为空');
}
console.log('使用登录token获取code...');
const response = await fetch('https://v3-feature.blacklake.cn/api/openapiadmin/domain/web/v1/access_token/_code', {
method: 'POST',
headers: {
'X-AUTH': loginToken,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`获取code失败: ${response.status}`);
}
const data = await response.json();
console.log('获取code响应:', data);
// 根据实际响应格式提取code
if (data.code === 200 || data.code === 0) {
const codeData = data.data || data.result || data;
const code = codeData.code ||
codeData.accessToken ||
data.code ||
data.accessToken;
if (code) {
console.log('成功获取code');
return code;
} else {
throw new Error('响应中没有找到code');
}
} else {
throw new Error(data.message || `获取code失败 (code: ${data.code})`);
}
} catch (error) {
console.error('获取code失败:', error);
throw error;
}
}
/**
* 登录并获取token
*/
async loginAndGetToken() {
try {
const loginData = {
type: AUTH_CONFIG.login.type,
username: AUTH_CONFIG.login.username,
code: AUTH_CONFIG.login.code,
password: AUTH_CONFIG.login.password
};
console.log('尝试登录获取token...');
const response = await fetch('https://v3-feature.blacklake.cn/api/user/domain/web/v1/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(loginData)
});
if (!response.ok) {
throw new Error(`登录失败: ${response.status}`);
}
const data = await response.json();
console.log('登录响应完整数据:', JSON.stringify(data, null, 2));
console.log('登录响应状态码:', response.status);
console.log('登录响应headers:', Object.fromEntries(response.headers.entries()));
// 检查响应是否成功(支持多种响应格式)
const isSuccess = data.success === true ||
data.code === 0 ||
data.code === 200 ||
(data.code === undefined && response.ok);
console.log('登录响应是否成功:', isSuccess, 'code:', data.code, 'success:', data.success);
if (isSuccess) {
// 尝试从不同位置提取token这是登录token用于后续获取code
const tokenData = data.data || data.result || data;
const loginToken = tokenData?.token ||
tokenData?.access_token ||
tokenData?.accessToken ||
data.token ||
data.access_token ||
data.accessToken;
if (loginToken) {
console.log('登录成功获取到登录token');
return { token: loginToken };
} else {
const errorMsg = '登录响应中没有找到token';
console.error(errorMsg, data);
throw new Error(errorMsg);
}
} else {
// 登录失败,抛出错误
const errorMsg = data.message || data.msg || `登录失败 (code: ${data.code})`;
console.error('登录失败:', errorMsg, data);
throw new Error(errorMsg);
}
} catch (error) {
console.error('登录接口调用失败:', error);
throw error; // 重新抛出错误
}
}
/**
* 获取当前token
* @returns {string|null}
*/
getToken() {
return this.token;
}
/**
* 检查是否已认证
* @returns {boolean}
*/
isAuthenticated() {
return !!this.token;
}
/**
* 保存token到本地存储
* @param {string} token - 要保存的token
*/
saveToken(token) {
try {
const tokenData = {
token: token,
timestamp: Date.now(),
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24小时后过期
};
localStorage.setItem(this.tokenKey, JSON.stringify(tokenData));
} catch (error) {
console.error('保存token失败:', error);
}
}
/**
* 从本地存储获取token
* @returns {string|null}
*/
getStoredToken() {
try {
const stored = localStorage.getItem(this.tokenKey);
if (!stored) {
return null;
}
const tokenData = JSON.parse(stored);
// 检查是否过期
if (tokenData.expiresAt && Date.now() > tokenData.expiresAt) {
console.log('存储的token已过期');
this.clearToken();
return null;
}
return tokenData.token;
} catch (error) {
console.error('获取存储token失败:', error);
return null;
}
}
/**
* 清除token
*/
clearToken() {
this.token = null;
try {
localStorage.removeItem(this.tokenKey);
} catch (error) {
console.error('清除token失败:', error);
}
}
/**
* 刷新token重新执行完整的认证流程
*/
async refreshToken() {
console.log('刷新token重新执行认证流程...');
this.clearToken();
await this.init();
}
/**
* 获取认证头部
* 根据接口文档,所有业务接口都需要使用 X-AUTH 头部,值为 user-access token
* @returns {Object}
*/
getAuthHeaders() {
const headers = {};
if (this.token) {
// 使用最终的user-access token
headers['X-AUTH'] = this.token;
}
return headers;
}
/**
* 处理认证错误
* @param {Response} response - HTTP响应对象
*/
async handleAuthError(response) {
if (response.status === 401 || response.status === 403) {
console.log('认证失败尝试刷新token');
showToast('认证已过期,正在重新获取...', 'info');
try {
await this.refreshToken();
return true; // 表示可以重试请求
} catch (error) {
console.error('刷新token失败:', error);
showToast('认证失败,请刷新页面', 'error');
return false;
}
}
return false;
}
/**
* 登出
*/
logout() {
this.clearToken();
showToast('已退出登录', 'info');
// 可以重定向到登录页面或刷新页面
setTimeout(() => {
window.location.reload();
}, 1000);
}
}
// 创建全局认证服务实例
const authService = new AuthService();