| | |
| | | } |
| | | |
| | | /** |
| | | * 获取所有分公司列表(parentId=100的部门) |
| | | */ |
| | | @GetMapping("/branch/all") |
| | | public AjaxResult listAllBranches() |
| | | { |
| | | SysDept query = new SysDept(); |
| | | query.setParentId(100L); |
| | | List<SysDept> depts = deptService.selectDeptList(query); |
| | | return success(depts); |
| | | } |
| | | |
| | | /** |
| | | * 查询部门列表(排除节点) |
| | | */ |
| | | @GetMapping("/list/exclude/{deptId}") |
| | |
| | | return error("未找到对应的用户信息"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 更新用户可管理分公司(通过oaOrderClass字段保存编码列表) |
| | | * 接收选中的分公司deptId列表,查询对应编码后合并写入oaOrderClass |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('system:user:edit')") |
| | | @Log(title = "用户管理-分公司配置", businessType = BusinessType.UPDATE) |
| | | @PutMapping("/branch/{userId}") |
| | | public AjaxResult updateUserBranch(@PathVariable Long userId, @RequestBody java.util.List<Long> deptIds) |
| | | { |
| | | userService.checkUserDataScope(userId); |
| | | SysUser user = userService.selectUserById(userId); |
| | | if (user == null) { |
| | | return error("用户不存在"); |
| | | } |
| | | // 根据deptIds查询分公司信息,收集编码 |
| | | java.util.Set<String> codeSet = new java.util.LinkedHashSet<>(); |
| | | if (deptIds != null && !deptIds.isEmpty()) { |
| | | SysDept queryDept = new SysDept(); |
| | | queryDept.setParentId(100L); |
| | | List<SysDept> allBranches = deptService.selectDeptList(queryDept); |
| | | java.util.Map<Long, SysDept> deptMap = new java.util.HashMap<>(); |
| | | for (SysDept d : allBranches) { |
| | | deptMap.put(d.getDeptId(), d); |
| | | } |
| | | for (Long deptId : deptIds) { |
| | | SysDept dept = deptMap.get(deptId); |
| | | if (dept != null) { |
| | | if (StringUtils.isNotEmpty(dept.getServiceOrderClass())) { |
| | | codeSet.add(dept.getServiceOrderClass().trim()); |
| | | } |
| | | if (StringUtils.isNotEmpty(dept.getDispatchOrderClass())) { |
| | | codeSet.add(dept.getDispatchOrderClass().trim()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | String newOaOrderClass = String.join(",", codeSet); |
| | | SysUser updateUser = new SysUser(); |
| | | updateUser.setUserId(userId); |
| | | updateUser.setOaOrderClass(newOaOrderClass); |
| | | updateUser.setUpdateBy(getUsername()); |
| | | return toAjax(userService.updateUserProfile(updateUser)); |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 查询车辆历史轨迹 |
| | | * 查询车辆历史轨迹(调用GPS平台接口) |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('system:gps:query')") |
| | | @GetMapping("/tracks") |
| | |
| | | return getAnonymousTracks(vehicleNo, beginTime, endTime); |
| | | } |
| | | |
| | | /** |
| | | * 从本地数据库查询车辆行驶轨迹(天地图轨迹页使用) |
| | | * 支持车牌号模糊查询 + 时间范围精确查询 |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('system:gps:list')") |
| | | @GetMapping("/tracksByPlate") |
| | | public TableDataInfo getTracksByPlate(String vehicleNo, String beginTime, String endTime) { |
| | | try { |
| | | if (vehicleNo == null || vehicleNo.trim().isEmpty()) { |
| | | return getDataTable(new ArrayList<>()); |
| | | } |
| | | VehicleGps query = new VehicleGps(); |
| | | query.setVehicleNo(vehicleNo.trim()); |
| | | if (beginTime != null && !beginTime.isEmpty()) { |
| | | query.setBeginTime(beginTime.replace("T", " ")); |
| | | } |
| | | if (endTime != null && !endTime.isEmpty()) { |
| | | query.setEndTime(endTime.replace("T", " ")); |
| | | } |
| | | query.setOrderByColumn("collect_time"); |
| | | query.setIsAsc("asc"); |
| | | List<VehicleGps> list = vehicleGpsService.selectVehicleGpsList(query); |
| | | return getDataTable(list); |
| | | } catch (Exception e) { |
| | | logger.error("从数据库查询车辆轨迹失败", e); |
| | | return getDataTable(new ArrayList<>()); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 匿名查询车辆历史轨迹 |
| New file |
| | |
| | | package com.ruoyi.web.controller.task; |
| | | |
| | | import com.ruoyi.common.core.controller.BaseController; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import com.ruoyi.common.utils.poi.ExcelUtil; |
| | | import com.ruoyi.system.domain.vo.DeptOrderStatVO; |
| | | import com.ruoyi.system.service.ISysTaskService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 分公司录单统计 Controller |
| | | */ |
| | | @RestController |
| | | @RequestMapping("/task/stat") |
| | | public class SysTaskStatController extends BaseController { |
| | | |
| | | @Autowired |
| | | private ISysTaskService sysTaskService; |
| | | |
| | | /** |
| | | * 按分公司、时间范围查询每天录单统计 |
| | | * |
| | | * @param deptIds 分公司ID列表,逗号分隔,为空则查全部 |
| | | * @param startDate 开始日期 yyyy-MM-dd |
| | | * @param endDate 结束日期 yyyy-MM-dd |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('task:stat:query')") |
| | | @GetMapping("/deptOrder") |
| | | public AjaxResult deptOrderStat( |
| | | @RequestParam(required = false) String deptIds, |
| | | @RequestParam String startDate, |
| | | @RequestParam String endDate) { |
| | | List<Long> deptIdList = parseDeptIds(deptIds); |
| | | List<DeptOrderStatVO> list = sysTaskService.selectDeptOrderStat(deptIdList, startDate, endDate); |
| | | return AjaxResult.success(list); |
| | | } |
| | | |
| | | /** |
| | | * 导出 Excel |
| | | */ |
| | | @PreAuthorize("@ss.hasPermi('task:stat:export')") |
| | | @GetMapping("/deptOrder/export") |
| | | public void exportDeptOrderStat( |
| | | HttpServletResponse response, |
| | | @RequestParam(required = false) String deptIds, |
| | | @RequestParam String startDate, |
| | | @RequestParam String endDate) { |
| | | List<Long> deptIdList = parseDeptIds(deptIds); |
| | | List<DeptOrderStatVO> list = sysTaskService.selectDeptOrderStat(deptIdList, startDate, endDate); |
| | | ExcelUtil<DeptOrderStatVO> util = new ExcelUtil<>(DeptOrderStatVO.class); |
| | | util.exportExcel(response, list, "分公司录单统计"); |
| | | } |
| | | |
| | | /** 解析逗号分隔的 deptIds 字符串为 Long 列表 */ |
| | | private List<Long> parseDeptIds(String deptIds) { |
| | | List<Long> result = new ArrayList<>(); |
| | | if (deptIds == null || deptIds.trim().isEmpty()) { |
| | | return result; |
| | | } |
| | | for (String id : deptIds.split(",")) { |
| | | try { |
| | | result.add(Long.parseLong(id.trim())); |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | } |
| | |
| | | # 从库数据源 |
| | | # SQL Server数据源 |
| | | sqlserver: |
| | | url: jdbc:sqlserver://120.25.98.119:1432;databaseName=came |
| | | url: jdbc:sqlserver://120.25.98.119:1432;databaseName=came;loginTimeout=60;queryTimeout=300 |
| | | username: camesa |
| | | password: camesa |
| | | driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver |
| | | enabled: true |
| | | validationQuery: SELECT 1 |
| | | # 连接超时时间(毫秒):获取连接最长等待时间,60秒 |
| | | connectTimeout: 60000 |
| | | # Socket读取超时(毫秒):SQL执行最长等待时间,5分钟 |
| | | socketTimeout: 300000 |
| | | slave: |
| | | # 从数据源开关/默认关闭 |
| | | enabled: false |
| | |
| | | # 从库数据源 |
| | | # SQL Server数据源 |
| | | sqlserver: |
| | | url: jdbc:sqlserver://39.108.160.52;databaseName=came |
| | | url: jdbc:sqlserver://39.108.160.52;databaseName=came;loginTimeout=60;queryTimeout=300 |
| | | username: camesa |
| | | password: camesa |
| | | driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver |
| | | enabled: true |
| | | validationQuery: SELECT 1 |
| | | # 连接超时时间(毫秒):获取连接最长等待时间,改为60秒 |
| | | connectTimeout: 60000 |
| | | # Socket读取超时(毫秒):SQL执行最长等待时间,改为5分钟 |
| | | socketTimeout: 300000 |
| | | slave: |
| | | # 从数据源开关/默认关闭 |
| | | enabled: false |
| | |
| | | basename: i18n/messages |
| | | profiles: |
| | | # 环境 dev|test|prod |
| | | active: dev |
| | | active: prod |
| | | # 文件上传 |
| | | servlet: |
| | | multipart: |
| New file |
| | |
| | | package com.ruoyi.system.domain.vo; |
| | | |
| | | import com.ruoyi.common.annotation.Excel; |
| | | |
| | | /** |
| | | * 分公司每日录单统计 VO |
| | | */ |
| | | public class DeptOrderStatVO { |
| | | |
| | | /** 分公司ID */ |
| | | private Long deptId; |
| | | |
| | | /** 分公司名称 */ |
| | | @Excel(name = "分公司") |
| | | private String deptName; |
| | | |
| | | /** 统计日期,格式 yyyy-MM-dd */ |
| | | @Excel(name = "日期") |
| | | private String statDate; |
| | | |
| | | /** 录单数量 */ |
| | | @Excel(name = "录单数量") |
| | | private Integer orderCount; |
| | | |
| | | public Long getDeptId() { return deptId; } |
| | | public void setDeptId(Long deptId) { this.deptId = deptId; } |
| | | |
| | | public String getDeptName() { return deptName; } |
| | | public void setDeptName(String deptName) { this.deptName = deptName; } |
| | | |
| | | public String getStatDate() { return statDate; } |
| | | public void setStatDate(String statDate) { this.statDate = statDate; } |
| | | |
| | | public Integer getOrderCount() { return orderCount; } |
| | | public void setOrderCount(Integer orderCount) { this.orderCount = orderCount; } |
| | | } |
| | |
| | | package com.ruoyi.system.listener; |
| | | |
| | | import com.ruoyi.common.core.domain.entity.SysDept; |
| | | import com.ruoyi.common.config.WechatConfig; |
| | | import com.ruoyi.common.utils.DeptUtil; |
| | | import com.ruoyi.common.utils.LongUtil; |
| | | import com.ruoyi.system.domain.*; |
| | |
| | | |
| | | @Autowired |
| | | private INotifyDispatchService notifyDispatchService; |
| | | |
| | | @Autowired |
| | | private IQyWechatService qyWechatService; |
| | | |
| | | @Autowired |
| | | private WechatConfig wechatConfig; |
| | | |
| | | /** 待准备状态 - 可以发送短信通知 */ |
| | | private static final String TASK_STATUS_PENDING = "PENDING"; |
| | |
| | | /** |
| | | * 监听任务分配事件 |
| | | * 创建通知任务,由通知分发服务决定发送渠道 |
| | | * 同时直接发送企业微信通知 |
| | | * |
| | | * @param event 任务分配事件 |
| | | */ |
| | |
| | | Long creatorId = task.getCreatorId(); |
| | | String taskStatus = task.getTaskStatus(); |
| | | task.setEmergencyInfo(emergency); |
| | | // 仅在待准备状态下发送通知 |
| | | if (!TASK_STATUS_PENDING.equals(taskStatus) && !TASK_STATUS_PREPARING.equals(taskStatus)) { |
| | | log.info("任务状态({})非待准备状态,跳过通知,taskId={}", taskStatus, event.getTaskId()); |
| | | return; |
| | | } |
| | | |
| | | |
| | | // 构建通知内容 |
| | | String notifyContent = buildNotifyContent(task, emergency); |
| | | this.sendDispatchNotify(event.getAssigneeIds(), creatorId, event.getTaskId(),task.getShowTaskCode(), notifyContent); |
| | | |
| | | // 直接发送企业微信通知给执行人员 |
| | | sendQyWechatNotifyToAssignees(event.getAssigneeIds(), creatorId, event.getTaskId(), notifyContent); |
| | | |
| | | // 同时走原有通知分发流程(站内消息等) |
| | | // 仅在待准备状态下发送其他通知 |
| | | if (TASK_STATUS_PENDING.equals(taskStatus) || TASK_STATUS_PREPARING.equals(taskStatus)) { |
| | | this.sendDispatchNotify(event.getAssigneeIds(), creatorId, event.getTaskId(), task.getShowTaskCode(), notifyContent); |
| | | } |
| | | |
| | | |
| | | } catch (Exception e) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 直接发送企业微信通知给执行人员 |
| | | */ |
| | | private void sendQyWechatNotifyToAssignees(List<Long> assigneeIds, Long creatorId, Long taskId, String content) { |
| | | String appId = wechatConfig.getAppId(); |
| | | String pathPage = "/pagesTask/detail?id=" + taskId; |
| | | int successCount = 0; |
| | | |
| | | for (Long assigneeId : assigneeIds) { |
| | | // 排除创建人 |
| | | if (creatorId != null && creatorId.equals(assigneeId)) { |
| | | log.debug("跳过创建人,不发送企业微信通知,userId={}", assigneeId); |
| | | continue; |
| | | } |
| | | |
| | | try { |
| | | boolean success = qyWechatService.sendNotifyMessage( |
| | | assigneeId, |
| | | "转运单任务派单通知", |
| | | content, |
| | | appId, |
| | | pathPage |
| | | ); |
| | | |
| | | if (success) { |
| | | successCount++; |
| | | log.info("企业微信派单通知发送成功,taskId={}, userId={}", taskId, assigneeId); |
| | | } else { |
| | | log.warn("企业微信派单通知发送失败,taskId={}, userId={}", taskId, assigneeId); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("企业微信派单通知发送异常,taskId={}, userId={}", taskId, assigneeId, e); |
| | | } |
| | | } |
| | | |
| | | log.info("企业微信派单通知发送完成,taskId={}, 成功数量={}/{}", taskId, successCount, assigneeIds.size()); |
| | | } |
| | | |
| | | /** |
| | | * 向执行人发送任务分配通知 |
| | | * @param assigneeIds |
| | | * @param creatorId |
| | |
| | | int countByTaskUserType(@Param("taskId") Long taskId, @Param("userId") Long userId, @Param("notifyType") String notifyType); |
| | | |
| | | /** |
| | | * 按taskId和通知类型查询所有通知任务 |
| | | * |
| | | * @param taskId 业务任务ID |
| | | * @param notifyType 通知类型 |
| | | * @return 通知任务列表 |
| | | */ |
| | | List<NotifyTask> selectByTaskIdAndType(@Param("taskId") Long taskId, @Param("notifyType") String notifyType); |
| | | |
| | | /** |
| | | * 新增通知任务 |
| | | * |
| | | * @param notifyTask 通知任务 |
| | |
| | | |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.ruoyi.common.annotation.DataSource; |
| | | import com.ruoyi.common.enums.DataSourceType; |
| | | import com.ruoyi.system.domain.SysTask; |
| | | import com.ruoyi.system.domain.vo.TaskQueryVO; |
| | | import com.ruoyi.system.domain.vo.DeptOrderStatVO; |
| | | import com.ruoyi.system.domain.vo.TaskStatisticsVO; |
| | | import org.apache.ibatis.annotations.Param; |
| | | |
| | |
| | | /** |
| | | * 查询车辆在指定时间范围内的任务列表 |
| | | * |
| | | * @param params包含vehicleId、startTime、endTime的参数Map |
| | | * @param params 包含vehicleId、startTime、endTime的参数Map |
| | | * @return 任务列表 |
| | | */ |
| | | public List<SysTask> selectVehicleTasksInTimeRange(java.util.Map<String, Object> params); |
| | | public List<SysTask> selectVehicleTasksInTimeRange(Map<String, Object> params); |
| | | |
| | | /** |
| | | * 优化的多码查询方法,关联sys_task_emergency表并计算dispatchCode和serviceCode |
| | |
| | | * @return 任务管理集合 |
| | | */ |
| | | public List<SysTask> selectSysTaskListByMultiCodeOptimized(TaskQueryVO queryVO); |
| | | /** |
| | | * 按分公司按天统计录单数量 |
| | | * |
| | | * @param deptIds 分公司ID列表(为null时查全部) |
| | | * @param startDate 开始日期,yyyy-MM-dd |
| | | * @param endDate 结束日期,yyyy-MM-dd |
| | | * @return 统计结果列表 |
| | | */ |
| | | List<DeptOrderStatVO> selectDeptOrderStat( |
| | | @Param("deptIds") List<Long> deptIds, |
| | | @Param("startDate") String startDate, |
| | | @Param("endDate") String endDate); |
| | | } |
| | |
| | | boolean existsNotifyTask(Long taskId, Long userId, String notifyType); |
| | | |
| | | /** |
| | | * 按taskId和通知类型查询所有通知任务 |
| | | * |
| | | * @param taskId 业务任务ID |
| | | * @param notifyType 通知类型 |
| | | * @return 通知任务列表 |
| | | */ |
| | | List<NotifyTask> selectByTaskIdAndType(Long taskId, String notifyType); |
| | | |
| | | /** |
| | | * 创建通知任务(带防重) |
| | | * 如果已存在则返回null,否则创建并返回 |
| | | * |
| | |
| | | */ |
| | | public boolean checkTaskDuplicate(String phone, String createDate); |
| | | |
| | | /** |
| | | * 按分公司按天统计录单数量 |
| | | * |
| | | * @param deptIds 分公司ID列表(为null或空时查全部) |
| | | * @param startDate 开始日期,yyyy-MM-dd |
| | | * @param endDate 结束日期,yyyy-MM-dd |
| | | * @return 统计结果列表 |
| | | */ |
| | | List<DeptOrderStatVO> selectDeptOrderStat(List<Long> deptIds, String startDate, String endDate); |
| | | |
| | | } |
| | |
| | | import com.ruoyi.common.core.domain.entity.SysDept; |
| | | import com.ruoyi.common.core.domain.entity.SysUser; |
| | | import com.ruoyi.common.utils.*; |
| | | import com.ruoyi.system.domain.SysTask; |
| | | import com.ruoyi.system.domain.SysTaskEmergency; |
| | | import com.ruoyi.system.domain.VehicleInfo; |
| | | import com.ruoyi.system.domain.enums.TaskStatus; |
| | |
| | | import com.ruoyi.system.mapper.VehicleInfoMapper; |
| | | import com.ruoyi.system.service.ISysUserService; |
| | | import com.ruoyi.system.service.IWechatTaskNotifyService; |
| | | import com.ruoyi.system.service.INotifyTaskService; |
| | | import com.ruoyi.system.service.INotifyDispatchService; |
| | | import com.ruoyi.system.domain.NotifyTask; |
| | | import com.ruoyi.system.utils.TaskStatusConverter; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.Date; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 旧系统转运单同步Service业务层处理 |
| | |
| | | @Autowired |
| | | private IWechatTaskNotifyService wechatTaskNotifyService; |
| | | |
| | | @Autowired |
| | | private INotifyTaskService notifyTaskService; |
| | | |
| | | @Autowired |
| | | private INotifyDispatchService notifyDispatchService; |
| | | |
| | | |
| | | |
| | | /** |
| | | * 同步指定日期范围的旧系统转运单到新系统 |
| | | * |
| | | * @param daysAgo 多少天前的数据(如7表示7天前的数据) |
| | | * 优化:将多天范围拆分为逐天循环,每次仅查询1天数据,避免大数据量导致SQL Server超时 |
| | | * |
| | | * @param daysAgo 多少天前的数据(如7表示同步最近7天的数据) |
| | | * @return 成功同步的转运单数量 |
| | | */ |
| | | @Override |
| | | public int syncLegacyTransferOrders(int daysAgo) { |
| | | // log.info("开始同步{}天前的旧系统转运单数据", daysAgo); |
| | | |
| | | try { |
| | | // 参数验证 |
| | | if (daysAgo <= 0) { |
| | | log.error("天数参数必须大于0"); |
| | | return 0; |
| | | } |
| | | |
| | | // 计算日期范围 |
| | | Date startDate = DateUtils.addDays(new Date(), -daysAgo); |
| | | String startDateStr = DateUtils.parseDateToStr("yyyy-MM-dd", startDate); |
| | | String endDateStr = DateUtils.parseDateToStr("yyyy-MM-dd", new Date()); |
| | | |
| | | // Keyset游标分页从 SQL Server 拉取转运单数据,每页 10 条,走主键索引彻底规避超时 |
| | | final int PAGE_SIZE = 5; |
| | | long lastId = 0L; // 游标:记录上一页最后一条的 ServiceOrdID,首次传 0 |
| | | int successCount = 0; |
| | | |
| | | log.info("[转运单同步] 开始同步,范围: 最近{}天", daysAgo); |
| | | int totalSuccessCount = 0; |
| | | int totalDays = daysAgo + 1; |
| | | |
| | | // 按天拆分,每次只同步1天的数据,避免大范围查询超时 |
| | | for (int i = daysAgo; i >= 0; i--) { |
| | | Date dayStart = DateUtils.addDays(new Date(), -i); |
| | | String dayStartStr = DateUtils.parseDateToStr("yyyy-MM-dd", dayStart) + " 00:00:00"; |
| | | String dayEndStr = DateUtils.parseDateToStr("yyyy-MM-dd", dayStart) + " 23:59:59"; |
| | | |
| | | int dayIndex = totalDays - i; |
| | | log.info("[转运单同步] 处理天 {}/{}: {}", dayIndex, totalDays, dayStartStr); |
| | | int daySuccessCount = syncSingleDayOrders(dayStartStr, dayEndStr); |
| | | totalSuccessCount += daySuccessCount; |
| | | log.info("[转运单同步] {} 完成,新增同步: {}条,累计: {}条", dayStartStr, daySuccessCount, totalSuccessCount); |
| | | } |
| | | |
| | | log.info("[转运单同步] 全部完成,共新增同步 {}条", totalSuccessCount); |
| | | return totalSuccessCount; |
| | | |
| | | } catch (Exception e) { |
| | | log.error("同步{}天前的旧系统转运单数据异常", daysAgo, e); |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 同步单天的转运单数据(Keyset游标分页) |
| | | * |
| | | * @param startDateStr 开始日期字符串(yyyy-MM-dd) |
| | | * @param endDateStr 结束日期字符串(yyyy-MM-dd) |
| | | * @return 成功同步的转运单数量 |
| | | */ |
| | | private int syncSingleDayOrders(String startDateStr, String endDateStr) { |
| | | final int PAGE_SIZE = 5; |
| | | long lastId = 0L; |
| | | int successCount = 0; |
| | | int pageNum = 0; |
| | | int totalProcessed = 0; |
| | | |
| | | try { |
| | | while (true) { |
| | | List<Map<String, Object>> transferOrders = legacyTransferSyncMapper.selectTransferOrders(startDateStr, endDateStr, lastId, PAGE_SIZE); |
| | | |
| | |
| | | break; |
| | | } |
| | | |
| | | pageNum++; |
| | | int totalCount = transferOrders.size(); |
| | | int processedCount = 0; |
| | | |
| | | log.info("[转运单同步] {} 第{}页,获取{}.条数据,lastId={}", startDateStr, pageNum, totalCount, lastId); |
| | | |
| | | for (Map<String, Object> order : transferOrders) { |
| | | processedCount++; |
| | | totalProcessed++; |
| | | try { |
| | | Long serviceOrdID = MapValueUtils.getLongValue(order, "ServiceOrdID"); |
| | | Long dispatchOrdID = MapValueUtils.getLongValue(order, "DispatchOrdID"); |
| | | |
| | | // 检查参数有效性 |
| | | if (serviceOrdID == null || serviceOrdID <= 0) { |
| | | log.warn("第{}条数据服务单ID为空,跳过处理", processedCount); |
| | | continue; |
| | | } |
| | | |
| | | // log.debug("正在处理第{}/{}条转运单: ServiceOrdID={}, DispatchOrdID={}", |
| | | // processedCount, totalCount, serviceOrdID, dispatchOrdID); |
| | | |
| | | // 检查是否已同步 |
| | | if (isTransferOrderSynced(serviceOrdID, dispatchOrdID)) { |
| | | // log.debug("转运单已同步,跳过: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID); |
| | | //进行更新操作 |
| | | log.debug("[转运单同步] 已存在,执行更新: ServiceOrdID={}", serviceOrdID); |
| | | updateTransferOrder(serviceOrdID, dispatchOrdID, order); |
| | | continue; |
| | | } |
| | | |
| | | // 同步单个转运单 |
| | | log.info("[转运单同步] 新增同步: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID); |
| | | boolean success = syncSingleTransferOrder(serviceOrdID, dispatchOrdID, order); |
| | | if (success) { |
| | | successCount++; |
| | | log.info("[转运单同步] 同步成功: ServiceOrdID={}, 当天新增累计: {}", serviceOrdID, successCount); |
| | | } else { |
| | | log.warn("[转运单同步] 同步失败: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID); |
| | | } |
| | | |
| | | // 控制同步频率,避免请求过快 |
| | |
| | | } catch (InterruptedException ie) { |
| | | log.warn("同步任务被中断"); |
| | | Thread.currentThread().interrupt(); |
| | | break; |
| | | return successCount; |
| | | } catch (Exception e) { |
| | | log.error("同步单个转运单失败: ServiceOrdID={}, DispatchOrdID={}", |
| | | MapValueUtils.getStringValue(order, "ServiceOrdID"), |
| | |
| | | } |
| | | } |
| | | |
| | | // log.info("同步完成,成功同步{}条转运单数据", successCount); |
| | | return successCount; |
| | | |
| | | log.info("[转运单同步] {} 分页完成,共处理: {}条,新增同步: {}条", startDateStr, totalProcessed, successCount); |
| | | } catch (Exception e) { |
| | | log.error("同步{}天前的旧系统转运单数据异常", daysAgo, e); |
| | | return 0; |
| | | log.error("同步单天转运单数据异常: date={}", startDateStr, e); |
| | | } |
| | | |
| | | return successCount; |
| | | } |
| | | |
| | | /** |
| | |
| | | // log.info("转运单同步成功: ServiceOrdID={}, DispatchOrdID={}, 创建的任务ID={}", serviceOrdID, dispatchOrdID, result); |
| | | |
| | | try { |
| | | notifyTransferOrderByWechat((long) result, serviceOrdID, dispatchOrdID, serviceOrdNo, ServiceOrd_CC_Time, dept, order); |
| | | // 直接使用方法头部已查询的 emergency 获取 taskId |
| | | Long taskId = emergency.getTaskId(); |
| | | if (taskId != null) { |
| | | notifyTransferOrderByWechat(taskId, serviceOrdID, dispatchOrdID, serviceOrdNo, ServiceOrd_CC_Time, dept, order); |
| | | } else { |
| | | log.warn("更新后找不到taskId,跳过通知: ServiceOrdID={}", serviceOrdID); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("转运单同步成功后发送微信通知失败: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID, e); |
| | | } |
| | |
| | | SysDept dept, |
| | | Map<String, Object> order) { |
| | | try { |
| | | // 获取通知接收人列表 |
| | | List<SysUser> receivers = getWechatNotifyUsers(dispatchOrdID, dept); |
| | | if (receivers == null || receivers.isEmpty()) { |
| | | // log.info("旧系统同步转运单无可用微信接收人,taskId={}", taskId); |
| | | // 1. 获取执行人列表 |
| | | List<TaskCreateVO.AssigneeInfo> assignees = queryAssignees(dispatchOrdID); |
| | | if (assignees.isEmpty()) { |
| | | log.info("旧系统同步转运单无执行人,taskId={}", taskId); |
| | | return; |
| | | } |
| | | |
| | | // 提取接收人 ID 列表 |
| | | List<Long> userIds = new ArrayList<>(); |
| | | for (SysUser user : receivers) { |
| | | if (user != null && user.getUserId() != null) { |
| | | userIds.add(user.getUserId()); |
| | | // 2. 查询任务获取showTaskCode |
| | | SysTask sysTask = sysTaskService.getTaskDetail(taskId); |
| | | String showTaskCode = sysTask != null ? sysTask.getShowTaskCode() : serviceOrdNo; |
| | | |
| | | // 3. 构建通知内容 |
| | | String notifyContent = buildLegacyNotifyContent(showTaskCode, serviceOrdCcTime, order); |
| | | |
| | | // 4. 查询该taskId已有的通知记录,收集已存在的userId集合 |
| | | List<NotifyTask> existingTasks = notifyTaskService.selectByTaskIdAndType(taskId, NotifyTask.NOTIFY_TYPE_TASK_ASSIGN); |
| | | Set<Long> existingUserIds = new HashSet<>(); |
| | | List<NotifyTask> pendingTasks = new ArrayList<>(); |
| | | if (existingTasks != null && !existingTasks.isEmpty()) { |
| | | for (NotifyTask t : existingTasks) { |
| | | existingUserIds.add(t.getUserId()); |
| | | // 将未完成的记录收集给待分发列表 |
| | | if (!NotifyTask.STATUS_COMPLETED.equals(t.getStatus())) { |
| | | pendingTasks.add(t); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 调用统一的微信通知服务 |
| | | int successCount = wechatTaskNotifyService.sendTaskNotifyMessage(taskId, userIds); |
| | | // log.info("旧系统同步转运单微信通知发送完成,taskId={}, 成功={}", taskId, successCount); |
| | | // 5. 只对新执行人创建通知任务 |
| | | List<NotifyTask> notifyTasks = new ArrayList<>(pendingTasks); |
| | | for (TaskCreateVO.AssigneeInfo assignee : assignees) { |
| | | if (assignee == null || assignee.getUserId() == null) { |
| | | continue; |
| | | } |
| | | // 该用户已有通知记录,跳过 |
| | | if (existingUserIds.contains(assignee.getUserId())) { |
| | | log.info("用户已有通知记录,跳过创建,taskId={}, userId={}", taskId, assignee.getUserId()); |
| | | continue; |
| | | } |
| | | |
| | | SysUser user = sysUserService.selectUserById(assignee.getUserId()); |
| | | if (user == null) { |
| | | log.warn("找不到执行人用户信息,userId={}", assignee.getUserId()); |
| | | continue; |
| | | } |
| | | |
| | | NotifyTask notifyTask = new NotifyTask(); |
| | | notifyTask.setTaskId(taskId); |
| | | notifyTask.setTaskCode(showTaskCode); |
| | | notifyTask.setNotifyType(NotifyTask.NOTIFY_TYPE_TASK_ASSIGN); |
| | | notifyTask.setUserId(user.getUserId()); |
| | | notifyTask.setUserName(user.getNickName()); |
| | | notifyTask.setUserPhone(user.getPhonenumber()); |
| | | notifyTask.setTitle("转运单任务派单通知"); |
| | | notifyTask.setContent(notifyContent); |
| | | notifyTask.setCreateBy("系统同步"); |
| | | |
| | | NotifyTask created = notifyTaskService.createNotifyTask(notifyTask); |
| | | if (created != null) { |
| | | notifyTasks.add(created); |
| | | log.info("创建通知任务成功,id={}, userId={}", created.getId(), user.getUserId()); |
| | | } |
| | | } |
| | | |
| | | // 6. 分发通知任务 |
| | | if (!notifyTasks.isEmpty()) { |
| | | int successCount = notifyDispatchService.dispatchNotifies(notifyTasks); |
| | | log.info("旧系统同步转运单通知分发完成,taskId={}, 分发数量={}, 成功数量={}", |
| | | taskId, notifyTasks.size(), successCount); |
| | | } else { |
| | | log.info("旧系统同步转运单无需新增通知,taskId={}", taskId); |
| | | } |
| | | |
| | | // 5. 同时保留原有的微信通知服务(兼容) |
| | | // List<Long> userIds = new ArrayList<>(); |
| | | // for (TaskCreateVO.AssigneeInfo assignee : assignees) { |
| | | // if (assignee != null && assignee.getUserId() != null) { |
| | | // userIds.add(assignee.getUserId()); |
| | | // } |
| | | // } |
| | | // if (!userIds.isEmpty()) { |
| | | // int wxCount = wechatTaskNotifyService.sendTaskNotifyMessage(taskId, userIds); |
| | | // log.info("旧系统同步转运单微信通知发送完成,taskId={}, 成功={}", taskId, wxCount); |
| | | // } |
| | | |
| | | } catch (Exception e) { |
| | | log.error("notifyTransferOrderByWechat发生异常, serviceOrdID={}, dispatchOrdID={}", serviceOrdID, dispatchOrdID, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 构建旧系统同步转运单的通知内容 |
| | | */ |
| | | private String buildLegacyNotifyContent(String serviceOrdNo, Date serviceOrdCcTime, Map<String, Object> order) { |
| | | StringBuilder content = new StringBuilder(); |
| | | content.append("您有新的转运任务,任务单号:").append(serviceOrdNo); |
| | | |
| | | // 出发时间 |
| | | if (serviceOrdCcTime != null) { |
| | | SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); |
| | | content.append(",出发时间:").append(df.format(serviceOrdCcTime)); |
| | | } |
| | | |
| | | // 出发地 |
| | | String departure = MapValueUtils.getStringValue(order, "ServiceOrdTraVia"); |
| | | if (StringUtils.isNotEmpty(departure)) { |
| | | content.append(",出发地:").append(departure); |
| | | } |
| | | |
| | | // 目的地 |
| | | String destination = MapValueUtils.getStringValue(order, "ServiceOrdTraEnd"); |
| | | if (StringUtils.isNotEmpty(destination)) { |
| | | content.append(",目的地:").append(destination); |
| | | } |
| | | |
| | | content.append(",请及时处理。"); |
| | | return content.toString(); |
| | | } |
| | | |
| | | private List<SysUser> getWechatNotifyUsers(Long dispatchOrdID, SysDept dept) { |
| | |
| | | List<SysUser> result = new ArrayList<>(); |
| | | |
| | | List<TaskCreateVO.AssigneeInfo> assignees = queryAssignees(dispatchOrdID); |
| | | if (assignees != null && !assignees.isEmpty()) { |
| | | if (!assignees.isEmpty()) { |
| | | for (TaskCreateVO.AssigneeInfo assigneeInfo : assignees) { |
| | | if (assigneeInfo == null || assigneeInfo.getUserId() == null) { |
| | | continue; |
| | |
| | | sendLog.setSendTime(DateUtils.getNowDate()); |
| | | sendLog.setSendContent(notifyTask.getContent()); |
| | | sendLog.setResponseMsg(errorMsg); |
| | | sendLog.setSendResult(success ? "发送成功" : ("发送失败: " + (errorMsg != null ? errorMsg : "未知原因"))); |
| | | |
| | | notifySendLogService.insertNotifySendLog(sendLog); |
| | | } catch (Exception e) { |
| | |
| | | */ |
| | | @Override |
| | | public boolean sendQyWechatMessage(NotifyTask notifyTask) { |
| | | try { |
| | | // 检查企业微信服务是否启用 |
| | | if (!qyWechatService.isEnabled()) { |
| | | log.info("企业微信服务已关闭,跳过发送"); |
| | | return false; |
| | | } |
| | | Long taskId= notifyTask.getTaskId(); |
| | | SysTaskEmergency emergency = this.sysEmergencyTaskService.selectSysTaskEmergencyByTaskId(taskId); |
| | | if(emergency==null){ |
| | | return false; |
| | | } |
| | | // Long dispatchOrderId = emergency.getLegacyDispatchOrdId(); |
| | | // String oldsiteUrl= sysConfigService.selectConfigByKey("oldsite.url"); |
| | | // if(oldsiteUrl==null){ |
| | | // oldsiteUrl="https://sys.966120.com.cn/m_DispatchOrder.gds?DispatchOrdID="; |
| | | // } |
| | | String appId=wechatConfig.getAppId(); |
| | | String pathPage="/pagesTask/detail?id="+taskId; |
| | | // 发送企业微信消息 |
| | | boolean success = qyWechatService.sendNotifyMessage( |
| | | notifyTask.getUserId(), |
| | | notifyTask.getTitle(), |
| | | notifyTask.getContent(),appId,pathPage |
| | | ); |
| | | |
| | | if (success) { |
| | | log.info("企业微信消息发送成功,userId={}", notifyTask.getUserId()); |
| | | } else { |
| | | log.warn("企业微信消息发送失败,userId={}", notifyTask.getUserId()); |
| | | } |
| | | return success; |
| | | } catch (Exception e) { |
| | | log.error("企业微信消息发送异常,taskId={}, userId={}", notifyTask.getTaskId(), notifyTask.getUserId(), e); |
| | | return false; |
| | | // 检查企业微信服务是否启用 |
| | | if (!qyWechatService.isEnabled()) { |
| | | throw new RuntimeException("企业微信服务未启用"); |
| | | } |
| | | Long taskId = notifyTask.getTaskId(); |
| | | SysTaskEmergency emergency = this.sysEmergencyTaskService.selectSysTaskEmergencyByTaskId(taskId); |
| | | if (emergency == null) { |
| | | throw new RuntimeException("找不到对应的急救任务信息,taskId=" + taskId); |
| | | } |
| | | // 检查用户是否绑定企业微信 |
| | | Long userId = notifyTask.getUserId(); |
| | | String qyUserId = qyWechatService.getQyUserIdByUserId(userId); |
| | | if (qyUserId == null || qyUserId.isEmpty()) { |
| | | throw new RuntimeException("用户未绑定企业微信ID,userId=" + userId); |
| | | } |
| | | String appId = wechatConfig.getAppId(); |
| | | String pathPage = "/pagesTask/detail?id=" + taskId; |
| | | // 发送企业微信消息 |
| | | boolean success = qyWechatService.sendNotifyMessage( |
| | | userId, |
| | | notifyTask.getTitle(), |
| | | notifyTask.getContent(), appId, pathPage |
| | | ); |
| | | if (success) { |
| | | log.info("企业微信消息发送成功,userId={}", userId); |
| | | } else { |
| | | throw new RuntimeException("企业微信API返回失败,userId=" + userId); |
| | | } |
| | | return true; |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 按taskId和通知类型查询所有通知任务 |
| | | */ |
| | | @Override |
| | | public List<NotifyTask> selectByTaskIdAndType(Long taskId, String notifyType) { |
| | | return notifyTaskMapper.selectByTaskIdAndType(taskId, notifyType); |
| | | } |
| | | |
| | | /** |
| | | * 创建通知任务(带防重) |
| | | */ |
| | | @Override |
| | |
| | | return count > 0; |
| | | } |
| | | |
| | | @Override |
| | | public List<DeptOrderStatVO> selectDeptOrderStat(List<Long> deptIds, String startDate, String endDate) { |
| | | return sysTaskMapper.selectDeptOrderStat( |
| | | (deptIds != null && !deptIds.isEmpty()) ? deptIds : null, |
| | | startDate, endDate); |
| | | } |
| | | |
| | | |
| | | |
| | | } |
| | |
| | | where task_id = #{taskId} and user_id = #{userId} and notify_type = #{notifyType} |
| | | </select> |
| | | |
| | | <select id="selectByTaskIdAndType" resultMap="NotifyTaskResult"> |
| | | <include refid="selectNotifyTaskVo"/> |
| | | where task_id = #{taskId} and notify_type = #{notifyType} |
| | | </select> |
| | | |
| | | <insert id="insertNotifyTask" parameterType="NotifyTask" useGeneratedKeys="true" keyProperty="id"> |
| | | insert into sys_notify_task ( |
| | | task_id, task_code, notify_type, user_id, user_name, user_phone, |
| | |
| | | END, |
| | | t.create_time desc |
| | | </select> |
| | | |
| | | <!-- 按分公司按天统计录单数量 --> |
| | | <select id="selectDeptOrderStat" resultType="com.ruoyi.system.domain.vo.DeptOrderStatVO"> |
| | | SELECT |
| | | d.dept_id AS deptId, |
| | | d.dept_name AS deptName, |
| | | DATE_FORMAT(t.create_time, '%Y-%m-%d') AS statDate, |
| | | COUNT(t.task_id) AS orderCount |
| | | FROM sys_task t |
| | | LEFT JOIN sys_dept d ON t.dept_id = d.dept_id |
| | | WHERE t.del_flag = '0' |
| | | AND DATE(t.create_time) BETWEEN #{startDate} AND #{endDate} |
| | | <if test="deptIds != null and deptIds.size() > 0"> |
| | | AND t.dept_id IN |
| | | <foreach collection="deptIds" item="id" open="(" separator="," close=")"> |
| | | #{id} |
| | | </foreach> |
| | | </if> |
| | | GROUP BY d.dept_id, d.dept_name, DATE_FORMAT(t.create_time, '%Y-%m-%d') |
| | | ORDER BY d.dept_name, statDate |
| | | </select> |
| | | </mapper> |
| | |
| | | <if test="direction != null "> and direction = #{direction}</if> |
| | | <if test="collectTime != null "> and collect_time = #{collectTime}</if> |
| | | <if test="beginTime != null and beginTime != ''"><!-- 开始时间检索 --> |
| | | AND date_format(collect_time,'%y%m%d') >= date_format(#{beginTime},'%y%m%d') |
| | | AND collect_time >= #{beginTime} |
| | | </if> |
| | | <if test="endTime != null and endTime != ''"><!-- 结束时间检索 --> |
| | | AND date_format(collect_time,'%y%m%d') <= date_format(#{endTime},'%y%m%d') |
| | | AND collect_time <= #{endTime} |
| | | </if> |
| | | </where> |
| | | order by collect_time desc |
| | |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 获取所有分公司列表(parentId=100的部门) |
| | | export function listAllBranches() { |
| | | return request({ |
| | | url: '/system/dept/branch/all', |
| | | method: 'get' |
| | | }) |
| | | } |
| | |
| | | endTime |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 按车牌号和时间范围查询轨迹(从本地数据库tb_vehicle_gps,天地图轨迹页使用) |
| | | export function getTracksByPlate(vehicleNo, beginTime, endTime) { |
| | | return request({ |
| | | url: '/system/gps/tracksByPlate', |
| | | method: 'get', |
| | | params: { |
| | | vehicleNo, |
| | | beginTime, |
| | | endTime |
| | | }, |
| | | timeout: 180000 |
| | | }) |
| | | } |
| | |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // 更新用户可管理分公司(传入deptId数组) |
| | | export function updateUserBranch(userId, deptIds) { |
| | | return request({ |
| | | url: '/system/user/branch/' + userId, |
| | | method: 'put', |
| | | data: deptIds |
| | | }) |
| | | } |
| | |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // ========== 分公司录单统计相关API ========== |
| | | |
| | | // 按分公司按天统计录单数量 |
| | | export function getDeptOrderStat(params) { |
| | | return request({ |
| | | url: '/task/stat/deptOrder', |
| | | method: 'get', |
| | | params |
| | | }) |
| | | } |
| | | |
| | | // 导出分公司录单统计 Excel |
| | | export function exportDeptOrderStat(params) { |
| | | return request({ |
| | | url: '/task/stat/deptOrder/export', |
| | | method: 'get', |
| | | params, |
| | | responseType: 'blob' |
| | | }) |
| | | } |
| | |
| | | component: (resolve) => require(['@/views/system/payInfoTest/index'], resolve), |
| | | hidden: true, |
| | | meta: { title: '支付信息测试', anonymous: true } |
| | | } |
| | | ,{ |
| | | }, |
| | | { |
| | | path: '/system/gps/mapNeed', |
| | | component: () => import('@/views/system/gps/mapNeed'), |
| | | name: 'GpsMapNeed', |
| | | meta: { title: '车辆轨迹', icon: 'map' } |
| | | }, |
| | | { |
| | | path: '/system/gps/trackMap', |
| | | component: () => import('@/views/system/gps/trackMap'), |
| | | name: 'GpsTrackMap', |
| | | meta: { title: '车辆行驶轨迹', icon: 'map' } |
| | | }, |
| | | |
| | | { |
| | |
| | | meta: { title: '任务详情', activeMenu: '/task/general' } |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | path: '/task/stat', |
| | | component: Layout, |
| | | hidden: false, |
| | | children: [ |
| | | { |
| | | path: 'index', |
| | | component: () => import('@/views/task/stat/index'), |
| | | name: 'DeptOrderStat', |
| | | meta: { title: '分公司录单统计', icon: 'chart', noCache: false } |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | |
| New file |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 查询条件 --> |
| | | <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px"> |
| | | <el-form-item label="车牌号" prop="vehicleNo"> |
| | | <el-autocomplete |
| | | v-model="queryParams.vehicleNo" |
| | | :fetch-suggestions="queryVehicleSearch" |
| | | placeholder="输入车牌号搜索" |
| | | size="small" |
| | | style="width: 200px" |
| | | clearable |
| | | :trigger-on-focus="true" |
| | | value-key="vehicleNo" |
| | | @select="handleVehicleSelect" |
| | | @keyup.enter.native="handleQuery" |
| | | > |
| | | <template slot-scope="{ item }"> |
| | | <span>{{ item.vehicleNo }}</span> |
| | | <span style="float:right;color:#909399;font-size:12px;margin-left:12px">{{ item.vehicleType || '' }}</span> |
| | | </template> |
| | | </el-autocomplete> |
| | | </el-form-item> |
| | | <el-form-item label="时间范围"> |
| | | <el-date-picker |
| | | v-model="dateRange" |
| | | size="small" |
| | | style="width: 340px" |
| | | value-format="yyyy-MM-dd HH:mm:ss" |
| | | type="datetimerange" |
| | | range-separator="-" |
| | | start-placeholder="开始时间" |
| | | end-placeholder="结束时间" |
| | | :default-time="['00:00:00', '23:59:59']" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="handleQuery">查询</el-button> |
| | | <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- 统计信息条 --> |
| | | <div class="stats-bar" v-if="gpsList.length > 0"> |
| | | <el-tag type="info" size="small">共 {{ gpsList.length }} 个轨迹点</el-tag> |
| | | <el-tag type="success" size="small" style="margin-left:8px">起点时间:{{ gpsList[0] && gpsList[0].collectTime }}</el-tag> |
| | | <el-tag type="warning" size="small" style="margin-left:8px">终点时间:{{ gpsList[gpsList.length-1] && gpsList[gpsList.length-1].collectTime }}</el-tag> |
| | | <el-button-group style="margin-left:16px"> |
| | | <el-button size="mini" @click="showPreviousSegment" :disabled="segmentIndex === 0"> |
| | | <i class="el-icon-arrow-left"></i> 上一段 |
| | | </el-button> |
| | | <el-button size="mini" @click="showNextSegment" :disabled="(segmentIndex + 1) * segmentSize >= gpsList.length"> |
| | | 下一段 <i class="el-icon-arrow-right"></i> |
| | | </el-button> |
| | | <el-button size="mini" style="margin-left:8px"> |
| | | 第 {{ segmentIndex + 1 }} / {{ Math.ceil(gpsList.length / segmentSize) }} 段 |
| | | </el-button> |
| | | </el-button-group> |
| | | <el-button-group style="margin-left:8px"> |
| | | <el-button size="mini" type="primary" icon="el-icon-video-play" :disabled="isPlaying" @click="startPlayback">回放</el-button> |
| | | <el-button size="mini" type="danger" icon="el-icon-video-pause" :disabled="!isPlaying" @click="stopPlayback">停止</el-button> |
| | | <el-select v-model="playSpeed" size="mini" style="width:90px;margin-left:4px"> |
| | | <el-option label="慢速" :value="1500" /> |
| | | <el-option label="正常" :value="800" /> |
| | | <el-option label="快速" :value="300" /> |
| | | <el-option label="极速" :value="80" /> |
| | | </el-select> |
| | | </el-button-group> |
| | | </div> |
| | | |
| | | <el-row :gutter="10" style="margin-top:10px"> |
| | | <!-- 左侧轨迹列表 --> |
| | | <el-col :span="6"> |
| | | <el-card class="track-list-card"> |
| | | <div slot="header"> |
| | | <span>轨迹点列表</span> |
| | | <el-tag size="mini" style="float:right" v-if="gpsList.length">{{ currentSegmentStart }}-{{ currentSegmentEnd }} / {{ gpsList.length }}</el-tag> |
| | | </div> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="currentSegmentList" |
| | | height="560" |
| | | size="mini" |
| | | highlight-current-row |
| | | @row-click="handleRowClick" |
| | | > |
| | | <el-table-column label="时间" prop="collectTime" min-width="100"> |
| | | <template slot-scope="scope"> |
| | | <span style="font-size:11px">{{ scope.row.collectTime }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="速度" prop="speed" width="65" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-tag size="mini" :type="getSpeedTagType(scope.row.speed)">{{ formatSpeed(scope.row.speed) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="经度" prop="longitude" width="80" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span style="font-size:11px">{{ scope.row.longitude }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <!-- 右侧天地图 --> |
| | | <el-col :span="18"> |
| | | <div style="position:relative"> |
| | | <div id="tianMapContainer" style="height:620px;border-radius:4px;overflow:hidden"></div> |
| | | <!-- 地图加载中提示 --> |
| | | <div v-if="mapLoading" class="map-loading-mask"> |
| | | <i class="el-icon-loading" style="font-size:32px;color:#409EFF"></i> |
| | | <p style="margin-top:8px;color:#409EFF">地图加载中...</p> |
| | | </div> |
| | | <!-- 无数据提示 --> |
| | | <div v-if="!mapLoading && gpsList.length === 0 && !loading" class="map-empty-tip"> |
| | | <i class="el-icon-map-location" style="font-size:48px;color:#909399"></i> |
| | | <p style="margin-top:8px;color:#909399">请输入车牌号和时间范围查询轨迹</p> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { getTracksByPlate } from '@/api/system/gps' |
| | | import { listVehicle } from '@/api/system/vehicle' |
| | | |
| | | // 天地图 API Key(与后端配置保持一致) |
| | | const TIAN_DI_TU_TK = '079300b89bce333ead2df1476c43ecb0' |
| | | |
| | | export default { |
| | | name: 'GpsTrackMap', |
| | | data() { |
| | | return { |
| | | loading: false, |
| | | mapLoading: true, |
| | | showSearch: true, |
| | | gpsList: [], |
| | | dateRange: [], |
| | | // 车牌号下拉搜索 |
| | | vehicleOptions: [], |
| | | vehicleSearchLoading: false, |
| | | queryParams: { |
| | | vehicleNo: '', |
| | | beginTime: null, |
| | | endTime: null |
| | | }, |
| | | // 地图对象 |
| | | map: null, |
| | | // 轨迹覆盖物 |
| | | trackPolyline: null, |
| | | markerStart: null, |
| | | markerEnd: null, |
| | | markerCurrent: null, |
| | | infoWindow: null, |
| | | // 分段 |
| | | segmentIndex: 0, |
| | | segmentSize: 200, |
| | | // 回放 |
| | | isPlaying: false, |
| | | playTimer: null, |
| | | playIndex: 0, |
| | | playSpeed: 800 |
| | | } |
| | | }, |
| | | computed: { |
| | | currentSegmentStart() { |
| | | return this.segmentIndex * this.segmentSize + 1 |
| | | }, |
| | | currentSegmentEnd() { |
| | | return Math.min((this.segmentIndex + 1) * this.segmentSize, this.gpsList.length) |
| | | }, |
| | | currentSegmentList() { |
| | | return this.gpsList.slice(this.currentSegmentStart - 1, this.currentSegmentEnd) |
| | | } |
| | | }, |
| | | mounted() { |
| | | // 初始化默认时间为今天 |
| | | this.initDefaultDateRange() |
| | | // 加载天地图 |
| | | this.loadTianDiTuScript().then(() => { |
| | | this.initMap() |
| | | }) |
| | | }, |
| | | methods: { |
| | | /** 车牌号自动完成搜索 */ |
| | | queryVehicleSearch(queryStr, callback) { |
| | | const keyword = queryStr || '' |
| | | listVehicle({ vehicleNo: keyword, pageSize: 30, pageNum: 1 }).then(res => { |
| | | const list = (res.rows || []).map(item => ({ |
| | | vehicleId: item.vehicleId, |
| | | vehicleNo: item.vehicleNo, |
| | | vehicleType: item.vehicleType, |
| | | value: item.vehicleNo |
| | | })) |
| | | callback(list) |
| | | }).catch(() => { |
| | | callback([]) |
| | | }) |
| | | }, |
| | | /** 选中车牌号 */ |
| | | handleVehicleSelect(item) { |
| | | this.queryParams.vehicleNo = item.vehicleNo |
| | | }, |
| | | /** 远程搜索车牌号 */ |
| | | remoteSearchVehicle(query) { |
| | | if (query === '' || query === null) { |
| | | this.vehicleOptions = [] |
| | | return |
| | | } |
| | | this.vehicleSearchLoading = true |
| | | listVehicle({ vehicleNo: query, pageSize: 20, pageNum: 1 }).then(res => { |
| | | this.vehicleOptions = res.rows || [] |
| | | this.vehicleSearchLoading = false |
| | | }).catch(() => { |
| | | this.vehicleSearchLoading = false |
| | | }) |
| | | }, |
| | | /** 初始化默认时间范围(今天) */ |
| | | initDefaultDateRange() { |
| | | const now = new Date() |
| | | const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0) |
| | | const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59) |
| | | this.dateRange = [this.formatDatetime(start), this.formatDatetime(end)] |
| | | }, |
| | | /** 格式化日期为字符串 */ |
| | | formatDatetime(date) { |
| | | const y = date.getFullYear() |
| | | const m = String(date.getMonth() + 1).padStart(2, '0') |
| | | const d = String(date.getDate()).padStart(2, '0') |
| | | const h = String(date.getHours()).padStart(2, '0') |
| | | const min = String(date.getMinutes()).padStart(2, '0') |
| | | const s = String(date.getSeconds()).padStart(2, '0') |
| | | return `${y}-${m}-${d} ${h}:${min}:${s}` |
| | | }, |
| | | /** 格式化速度显示 */ |
| | | formatSpeed(speed) { |
| | | if (speed == null) return '0' |
| | | return parseFloat(speed).toFixed(1) |
| | | }, |
| | | /** 速度标签类型 */ |
| | | getSpeedTagType(speed) { |
| | | const v = parseFloat(speed) || 0 |
| | | if (v === 0) return 'info' |
| | | if (v < 60) return 'success' |
| | | if (v < 100) return 'warning' |
| | | return 'danger' |
| | | }, |
| | | /** 加载天地图 JS API */ |
| | | loadTianDiTuScript() { |
| | | return new Promise((resolve, reject) => { |
| | | if (window.T) { |
| | | resolve() |
| | | return |
| | | } |
| | | const script = document.createElement('script') |
| | | script.type = 'text/javascript' |
| | | script.src = `http://api.tianditu.gov.cn/api?v=4.0&tk=${TIAN_DI_TU_TK}` |
| | | script.onload = () => { |
| | | // 等待 T 对象可用 |
| | | const checkT = setInterval(() => { |
| | | if (window.T) { |
| | | clearInterval(checkT) |
| | | resolve() |
| | | } |
| | | }, 100) |
| | | } |
| | | script.onerror = (err) => { |
| | | console.error('天地图脚本加载失败', err) |
| | | reject(err) |
| | | } |
| | | document.head.appendChild(script) |
| | | }) |
| | | }, |
| | | /** 初始化天地图 */ |
| | | initMap() { |
| | | try { |
| | | this.map = new T.Map('tianMapContainer') |
| | | // 默认居中广州 |
| | | this.map.centerAndZoom(new T.LngLat(113.33, 23.12), 11) |
| | | // 添加控件 |
| | | this.map.addControl(new T.Control.Zoom()) |
| | | this.map.addControl(new T.Control.Scale()) |
| | | this.mapLoading = false |
| | | } catch (e) { |
| | | console.error('天地图初始化失败', e) |
| | | this.$message.error('地图初始化失败,请刷新重试') |
| | | this.mapLoading = false |
| | | } |
| | | }, |
| | | /** 查询按钮 */ |
| | | handleQuery() { |
| | | if (!this.queryParams.vehicleNo || !this.queryParams.vehicleNo.trim()) { |
| | | this.$message.warning('请输入车牌号') |
| | | return |
| | | } |
| | | if (!this.dateRange || this.dateRange.length !== 2) { |
| | | this.$message.warning('请选择时间范围') |
| | | return |
| | | } |
| | | this.queryParams.beginTime = this.dateRange[0] |
| | | this.queryParams.endTime = this.dateRange[1] |
| | | this.segmentIndex = 0 |
| | | this.stopPlayback() |
| | | this.getTrackData() |
| | | }, |
| | | /** 重置 */ |
| | | resetQuery() { |
| | | this.$refs.queryForm.resetFields() |
| | | this.initDefaultDateRange() |
| | | this.gpsList = [] |
| | | this.segmentIndex = 0 |
| | | this.stopPlayback() |
| | | this.clearMap() |
| | | }, |
| | | /** 获取轨迹数据 */ |
| | | getTrackData() { |
| | | this.loading = true |
| | | getTracksByPlate( |
| | | this.queryParams.vehicleNo.trim(), |
| | | this.queryParams.beginTime, |
| | | this.queryParams.endTime |
| | | ).then(res => { |
| | | this.gpsList = (res.rows || []).sort((a, b) => { |
| | | return new Date(a.collectTime) - new Date(b.collectTime) |
| | | }) |
| | | this.loading = false |
| | | if (this.gpsList.length === 0) { |
| | | this.$message.info('该时间段内未查询到轨迹数据') |
| | | this.clearMap() |
| | | } else { |
| | | this.$message.success(`共查询到 ${this.gpsList.length} 个轨迹点`) |
| | | this.drawTrack() |
| | | } |
| | | }).catch(err => { |
| | | this.loading = false |
| | | this.$message.error('查询轨迹失败:' + (err.message || '未知错误')) |
| | | }) |
| | | }, |
| | | /** 清空地图覆盖物 */ |
| | | clearMap() { |
| | | if (!this.map) return |
| | | this.map.clearOverLays() |
| | | this.trackPolyline = null |
| | | this.markerStart = null |
| | | this.markerEnd = null |
| | | this.markerCurrent = null |
| | | }, |
| | | /** 绘制轨迹 */ |
| | | drawTrack() { |
| | | if (!this.map) return |
| | | this.clearMap() |
| | | |
| | | const segment = this.currentSegmentList |
| | | if (!segment || segment.length === 0) return |
| | | |
| | | // 构建轨迹点数组 |
| | | const points = segment |
| | | .filter(p => p.longitude != null && p.latitude != null) |
| | | .map(p => new T.LngLat(p.longitude, p.latitude)) |
| | | |
| | | if (points.length < 2) { |
| | | this.$message.warning('有效轨迹点不足,无法绘制') |
| | | return |
| | | } |
| | | |
| | | // 绘制轨迹线 |
| | | this.trackPolyline = new T.Polyline(points, { |
| | | color: '#3388ff', |
| | | weight: 4, |
| | | opacity: 0.85 |
| | | }) |
| | | this.map.addOverLay(this.trackPolyline) |
| | | |
| | | // 起点标记 |
| | | const startPoint = points[0] |
| | | const startMarker = this.createStartMarker(startPoint, segment[0]) |
| | | this.map.addOverLay(startMarker) |
| | | this.markerStart = startMarker |
| | | |
| | | // 终点标记(车辆图标) |
| | | const endIdx = segment.length - 1 |
| | | const endPoint = points[points.length - 1] |
| | | const endMarker = this.createVehicleMarker(endPoint, segment[endIdx]) |
| | | this.map.addOverLay(endMarker) |
| | | this.markerEnd = endMarker |
| | | |
| | | // 自适应视野 |
| | | this.map.setViewport(points) |
| | | }, |
| | | /** 创建起点标记 */ |
| | | createStartMarker(lngLat, data) { |
| | | const icon = new T.Icon({ |
| | | iconUrl: this.createSvgIconUrl('#2ecc71', '起'), |
| | | iconSize: new T.Point(28, 28), |
| | | iconAnchor: new T.Point(14, 14) |
| | | }) |
| | | const marker = new T.Marker(lngLat, { icon }) |
| | | marker.addEventListener('click', () => { |
| | | this.showInfoWindow(lngLat, data, '起点') |
| | | }) |
| | | return marker |
| | | }, |
| | | /** 创建车辆标记 */ |
| | | createVehicleMarker(lngLat, data) { |
| | | const direction = data.direction || 0 |
| | | const icon = new T.Icon({ |
| | | iconUrl: this.createCarIconUrl(direction), |
| | | iconSize: new T.Point(32, 32), |
| | | iconAnchor: new T.Point(16, 16) |
| | | }) |
| | | const marker = new T.Marker(lngLat, { icon }) |
| | | marker.addEventListener('click', () => { |
| | | this.showInfoWindow(lngLat, data, '当前位置') |
| | | }) |
| | | return marker |
| | | }, |
| | | /** 生成起点/终点 SVG 图标 URL */ |
| | | createSvgIconUrl(color, text) { |
| | | const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28"> |
| | | <circle cx="14" cy="14" r="13" fill="${color}" stroke="white" stroke-width="2"/> |
| | | <text x="14" y="18" text-anchor="middle" fill="white" font-size="12" font-weight="bold">${text}</text> |
| | | </svg>` |
| | | return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) |
| | | }, |
| | | /** 生成汽车图标 SVG URL(带方向旋转) */ |
| | | createCarIconUrl(direction) { |
| | | const deg = direction || 0 |
| | | const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"> |
| | | <g transform="rotate(${deg}, 16, 16)"> |
| | | <ellipse cx="16" cy="16" rx="10" ry="13" fill="#3388ff" stroke="white" stroke-width="2"/> |
| | | <polygon points="16,3 21,12 11,12" fill="white"/> |
| | | <rect x="11" y="22" width="4" height="3" rx="1" fill="white" opacity="0.8"/> |
| | | <rect x="17" y="22" width="4" height="3" rx="1" fill="white" opacity="0.8"/> |
| | | </g> |
| | | </svg>` |
| | | return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) |
| | | }, |
| | | /** 显示信息窗口 */ |
| | | showInfoWindow(lngLat, data, label) { |
| | | if (!this.map) return |
| | | if (this.infoWindow) { |
| | | this.map.closeInfoWindow() |
| | | } |
| | | const content = ` |
| | | <div style="padding:8px;min-width:180px;line-height:1.8"> |
| | | <b style="color:#3388ff">${label || ''}</b><br/> |
| | | <span>车牌:${data.vehicleNo || '-'}</span><br/> |
| | | <span>时间:${data.collectTime || '-'}</span><br/> |
| | | <span>速度:${this.formatSpeed(data.speed)} km/h</span><br/> |
| | | <span>方向:${data.direction || 0}°</span><br/> |
| | | <span>经度:${data.longitude}</span><br/> |
| | | <span>纬度:${data.latitude}</span> |
| | | </div> |
| | | ` |
| | | this.infoWindow = new T.InfoWindow(content, { |
| | | offset: new T.Point(0, -15) |
| | | }) |
| | | this.map.openInfoWindow(this.infoWindow, lngLat) |
| | | }, |
| | | /** 点击列表行 */ |
| | | handleRowClick(row) { |
| | | if (!row || row.longitude == null) return |
| | | const lngLat = new T.LngLat(row.longitude, row.latitude) |
| | | this.map.panTo(lngLat) |
| | | this.map.setZoom(15) |
| | | this.showInfoWindow(lngLat, row, '选中点') |
| | | }, |
| | | /** 上一段 */ |
| | | showPreviousSegment() { |
| | | if (this.segmentIndex > 0) { |
| | | this.segmentIndex-- |
| | | this.stopPlayback() |
| | | this.drawTrack() |
| | | } |
| | | }, |
| | | /** 下一段 */ |
| | | showNextSegment() { |
| | | if ((this.segmentIndex + 1) * this.segmentSize < this.gpsList.length) { |
| | | this.segmentIndex++ |
| | | this.stopPlayback() |
| | | this.drawTrack() |
| | | } |
| | | }, |
| | | /** 开始回放 */ |
| | | startPlayback() { |
| | | if (this.isPlaying || !this.gpsList.length) return |
| | | this.isPlaying = true |
| | | this.playIndex = this.currentSegmentStart - 1 |
| | | this.playNextPoint() |
| | | }, |
| | | /** 回放下一个点 */ |
| | | playNextPoint() { |
| | | if (!this.isPlaying) return |
| | | const allList = this.gpsList |
| | | if (this.playIndex >= this.currentSegmentEnd) { |
| | | this.stopPlayback() |
| | | this.$message.success('轨迹回放完成') |
| | | return |
| | | } |
| | | const item = allList[this.playIndex] |
| | | if (item && item.longitude != null) { |
| | | const lngLat = new T.LngLat(item.longitude, item.latitude) |
| | | // 移除旧的回放标记 |
| | | if (this.markerCurrent) { |
| | | this.map.removeOverLay(this.markerCurrent) |
| | | } |
| | | const marker = this.createVehicleMarker(lngLat, item) |
| | | this.map.addOverLay(marker) |
| | | this.markerCurrent = marker |
| | | this.map.panTo(lngLat) |
| | | } |
| | | this.playIndex++ |
| | | this.playTimer = setTimeout(() => this.playNextPoint(), this.playSpeed) |
| | | }, |
| | | /** 停止回放 */ |
| | | stopPlayback() { |
| | | if (this.playTimer) { |
| | | clearTimeout(this.playTimer) |
| | | this.playTimer = null |
| | | } |
| | | this.isPlaying = false |
| | | } |
| | | }, |
| | | beforeDestroy() { |
| | | this.stopPlayback() |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .stats-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 4px; |
| | | padding: 8px 12px; |
| | | background: #f5f7fa; |
| | | border-radius: 4px; |
| | | margin-bottom: 4px; |
| | | } |
| | | .track-list-card { |
| | | height: 660px; |
| | | } |
| | | .track-list-card >>> .el-card__body { |
| | | padding: 0; |
| | | overflow: hidden; |
| | | } |
| | | .map-loading-mask { |
| | | position: absolute; |
| | | top: 0; left: 0; right: 0; bottom: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background: rgba(255,255,255,0.85); |
| | | z-index: 999; |
| | | } |
| | | .map-empty-tip { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | text-align: center; |
| | | pointer-events: none; |
| | | z-index: 10; |
| | | } |
| | | </style> |
| | |
| | | </el-dialog> |
| | | |
| | | <!-- 新增:用户管理分公司配置对话框 --> |
| | | <el-dialog title="配置管理分公司" :visible.sync="branchDialog.open" width="500px" append-to-body> |
| | | <el-form label-width="100px"> |
| | | <el-form-item label="可管理分公司"> |
| | | <el-dialog title="配置管理分公司" :visible.sync="branchDialog.open" width="520px" append-to-body> |
| | | <el-form label-width="100px" v-loading="branchDialog.loading"> |
| | | <el-form-item label="已关联分公司"> |
| | | <div style="margin-bottom:8px;color:#666;font-size:12px;">OA自动关联(只读):</div> |
| | | <div> |
| | | <el-tag v-for="bc in branchDialog.companies" :key="bc.deptId" type="info" size="small" style="margin-right:6px;margin-bottom:6px">{{ bc.deptName }}</el-tag> |
| | | <span v-if="branchDialog.companies.length === 0" style="color:#999">暂无分公司</span> |
| | | <el-tag v-for="bc in branchDialog.companies" :key="'oa-'+bc.deptId" type="success" size="small" style="margin-right:6px;margin-bottom:6px">{{ bc.deptName }}</el-tag> |
| | | <span v-if="branchDialog.companies.length === 0" style="color:#999;font-size:12px">无OA自动关联</span> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="手动添加"> |
| | | <div style="margin-bottom:6px;color:#666;font-size:12px;">选择需要额外添加的分公司:</div> |
| | | <el-checkbox-group v-model="branchDialog.selectedDeptIds"> |
| | | <el-checkbox |
| | | v-for="branch in branchDialog.allBranches" |
| | | :key="branch.deptId" |
| | | :label="branch.deptId" |
| | | style="display:block;margin-bottom:4px;" |
| | | >{{ branch.deptName }}</el-checkbox> |
| | | </el-checkbox-group> |
| | | <div v-if="branchDialog.allBranches.length === 0" style="color:#999;font-size:12px">暂无分公司数据</div> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div slot="footer" class="dialog-footer"> |
| | | <el-button type="primary" @click="submitBranchCompanies" :loading="branchDialog.loading">保 存</el-button> |
| | | <el-button @click="branchDialog.open = false">关 闭</el-button> |
| | | </div> |
| | | </el-dialog> |
| | |
| | | |
| | | <script> |
| | | import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd, changeUserStatus, deptTreeSelect } from "@/api/system/user"; |
| | | import { updateUserBranch } from "@/api/system/user"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import Treeselect from "@riophae/vue-treeselect"; |
| | | import "@riophae/vue-treeselect/dist/vue-treeselect.css"; |
| | | import { Splitpanes, Pane } from "splitpanes"; |
| | | import "splitpanes/dist/splitpanes.css"; |
| | | import request from "@/utils/request"; |
| | | import { listDept, listBranchByUser } from "@/api/system/dept"; |
| | | import { listDept, listBranchByUser, listAllBranches } from "@/api/system/dept"; |
| | | |
| | | export default { |
| | | name: "User", |
| | |
| | | branchDialog: { |
| | | open: false, |
| | | userId: null, |
| | | deptOptions: [], |
| | | allBranches: [], |
| | | selectedDeptIds: [], |
| | | companies: [] |
| | | companies: [], |
| | | loading: false |
| | | }, |
| | | // 基于 oa_order_class 计算的分公司列表(只读展示) |
| | | userBranchCompanies: [] |
| | |
| | | handleManageBranch(row) { |
| | | const userId = row.userId; |
| | | this.branchDialog.userId = userId; |
| | | // 加载分公司列表(OA自动控制,只读展示) |
| | | listBranchByUser(userId).then(res => { |
| | | const list = res.data || []; |
| | | this.branchDialog.companies = (list || []).map(d => ({ deptId: d.deptId, deptName: d.deptName })); |
| | | this.branchDialog.open = true; |
| | | this.branchDialog.companies = []; |
| | | this.branchDialog.allBranches = []; |
| | | this.branchDialog.selectedDeptIds = []; |
| | | this.branchDialog.loading = true; |
| | | this.branchDialog.open = true; |
| | | // 并行加载:当前用户OA关联分公司 + 所有分公司列表 |
| | | const p1 = listBranchByUser(userId).then(res => { |
| | | return (res.data || []).map(d => ({ deptId: d.deptId, deptName: d.deptName })); |
| | | }); |
| | | const p2 = listAllBranches().then(res => { |
| | | return res.data || []; |
| | | }); |
| | | Promise.all([p1, p2]).then(([oaCompanies, allBranches]) => { |
| | | this.branchDialog.companies = oaCompanies; |
| | | this.branchDialog.allBranches = allBranches; |
| | | // 初始化已选:从 userList 中找到该用户的 oaOrderClass,匹配分公司 |
| | | const userRow = (this.userList || []).find(u => u.userId === userId); |
| | | if (userRow && userRow.oaOrderClass) { |
| | | const codes = userRow.oaOrderClass.split(',').map(s => s.trim()).filter(s => s); |
| | | const codeSet = new Set(codes); |
| | | const selected = allBranches |
| | | .filter(d => { |
| | | return (d.serviceOrderClass && codeSet.has(d.serviceOrderClass.trim())) |
| | | || (d.dispatchOrderClass && codeSet.has(d.dispatchOrderClass.trim())); |
| | | }) |
| | | .map(d => d.deptId); |
| | | this.branchDialog.selectedDeptIds = selected; |
| | | } |
| | | this.branchDialog.loading = false; |
| | | }).catch(() => { |
| | | this.$modal.msgError('加载分公司配置失败'); |
| | | this.branchDialog.loading = false; |
| | | }); |
| | | }, |
| | | /** 保存分公司配置 */ |
| | | // 已取消保存,OA自动控制(仅只读展示) |
| | | // submitBranchCompanies() { |
| | | // const userId = this.branchDialog.userId; |
| | | // const deptIds = this.branchDialog.selectedDeptIds || []; |
| | | // // 保留占位,避免误调用 |
| | | // }, |
| | | submitBranchCompanies() { |
| | | this.branchDialog.loading = true; |
| | | updateUserBranch(this.branchDialog.userId, this.branchDialog.selectedDeptIds).then(() => { |
| | | this.$modal.msgSuccess('分公司配置保存成功'); |
| | | this.branchDialog.loading = false; |
| | | this.branchDialog.open = false; |
| | | this.getList(); |
| | | }).catch(() => { |
| | | this.$modal.msgError('保存分公司配置失败'); |
| | | this.branchDialog.loading = false; |
| | | }); |
| | | }, |
| | | /** 提交按钮 */ |
| | | submitForm: function() { |
| | | this.$refs["form"].validate(valid => { |
| New file |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 查询条件 --> |
| | | <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px"> |
| | | <el-form-item label="分公司" prop="deptIds"> |
| | | <el-select |
| | | v-model="queryParams.deptIds" |
| | | placeholder="全部分公司" |
| | | clearable |
| | | filterable |
| | | multiple |
| | | collapse-tags |
| | | style="width: 240px" |
| | | > |
| | | <el-option |
| | | v-for="dept in branchList" |
| | | :key="dept.deptId" |
| | | :label="dept.deptName" |
| | | :value="dept.deptId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="时间范围" prop="dateRange"> |
| | | <el-date-picker |
| | | v-model="dateRange" |
| | | type="daterange" |
| | | value-format="yyyy-MM-dd" |
| | | range-separator="-" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | style="width: 240px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button> |
| | | <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- 操作栏 --> |
| | | <el-row :gutter="10" class="mb8"> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="warning" |
| | | plain |
| | | icon="el-icon-download" |
| | | size="mini" |
| | | :loading="exportLoading" |
| | | @click="handleExport" |
| | | v-hasPermi="['task:stat:export']" |
| | | >导出Excel</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 数据表格 --> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | border |
| | | style="width: 100%" |
| | | :span-method="mergeDeptRows" |
| | | > |
| | | <el-table-column label="分公司" prop="deptName" align="center" min-width="140" /> |
| | | <el-table-column label="日期" prop="statDate" align="center" width="120" /> |
| | | <el-table-column label="录单数量" prop="orderCount" align="center" width="120"> |
| | | <template slot-scope="scope"> |
| | | <el-tag type="primary">{{ scope.row.orderCount }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- 汇总信息 --> |
| | | <div v-if="tableData.length > 0" style="margin-top: 12px; color: #606266; font-size: 13px;"> |
| | | 共 <strong>{{ tableData.length }}</strong> 条记录,合计录单 |
| | | <strong style="color: #E6A23C; font-size: 16px;">{{ totalCount }}</strong> 单 |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { getDeptOrderStat, exportDeptOrderStat } from "@/api/task"; |
| | | import { listAllBranches } from "@/api/system/dept"; |
| | | |
| | | export default { |
| | | name: "DeptOrderStat", |
| | | data() { |
| | | const now = new Date(); |
| | | const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); |
| | | const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; |
| | | return { |
| | | loading: false, |
| | | exportLoading: false, |
| | | tableData: [], |
| | | branchList: [], |
| | | // 默认选择当月 |
| | | dateRange: [fmt(firstDay), fmt(now)], |
| | | queryParams: { |
| | | deptIds: [] |
| | | }, |
| | | // 合并行相关 |
| | | mergeMap: {} |
| | | }; |
| | | }, |
| | | computed: { |
| | | totalCount() { |
| | | return this.tableData.reduce((sum, row) => sum + (row.orderCount || 0), 0); |
| | | } |
| | | }, |
| | | created() { |
| | | this.loadBranchList(); |
| | | this.getList(); |
| | | }, |
| | | methods: { |
| | | /** 加载分公司列表 */ |
| | | loadBranchList() { |
| | | listAllBranches().then(res => { |
| | | this.branchList = res.data || []; |
| | | }).catch(() => { |
| | | this.branchList = []; |
| | | }); |
| | | }, |
| | | |
| | | /** 查询统计数据 */ |
| | | getList() { |
| | | if (!this.dateRange || this.dateRange.length !== 2) { |
| | | this.$message.warning("请选择时间范围"); |
| | | return; |
| | | } |
| | | this.loading = true; |
| | | const params = { |
| | | startDate: this.dateRange[0], |
| | | endDate: this.dateRange[1], |
| | | deptIds: this.queryParams.deptIds && this.queryParams.deptIds.length > 0 |
| | | ? this.queryParams.deptIds.join(",") |
| | | : undefined |
| | | }; |
| | | getDeptOrderStat(params).then(res => { |
| | | this.tableData = res.data || []; |
| | | this.buildMergeMap(); |
| | | this.loading = false; |
| | | }).catch(() => { |
| | | this.loading = false; |
| | | }); |
| | | }, |
| | | |
| | | /** 构建合并行映射 */ |
| | | buildMergeMap() { |
| | | const map = {}; |
| | | const data = this.tableData; |
| | | let i = 0; |
| | | while (i < data.length) { |
| | | let j = i; |
| | | while (j < data.length && data[j].deptName === data[i].deptName) { |
| | | j++; |
| | | } |
| | | map[i] = j - i; |
| | | for (let k = i + 1; k < j; k++) { |
| | | map[k] = 0; |
| | | } |
| | | i = j; |
| | | } |
| | | this.mergeMap = map; |
| | | }, |
| | | |
| | | /** 合并分公司列(相同分公司行合并) */ |
| | | mergeDeptRows({ row, column, rowIndex, columnIndex }) { |
| | | if (columnIndex === 0) { |
| | | const span = this.mergeMap[rowIndex]; |
| | | if (span !== undefined) { |
| | | return { rowspan: span, colspan: span === 0 ? 0 : 1 }; |
| | | } |
| | | } |
| | | }, |
| | | |
| | | /** 搜索 */ |
| | | handleQuery() { |
| | | this.getList(); |
| | | }, |
| | | |
| | | /** 重置 */ |
| | | resetQuery() { |
| | | const now = new Date(); |
| | | const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); |
| | | const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; |
| | | this.dateRange = [fmt(firstDay), fmt(now)]; |
| | | this.queryParams.deptIds = []; |
| | | this.getList(); |
| | | }, |
| | | |
| | | /** 导出 Excel */ |
| | | handleExport() { |
| | | if (!this.dateRange || this.dateRange.length !== 2) { |
| | | this.$message.warning("请选择时间范围"); |
| | | return; |
| | | } |
| | | this.exportLoading = true; |
| | | const params = { |
| | | startDate: this.dateRange[0], |
| | | endDate: this.dateRange[1], |
| | | deptIds: this.queryParams.deptIds && this.queryParams.deptIds.length > 0 |
| | | ? this.queryParams.deptIds.join(",") |
| | | : undefined |
| | | }; |
| | | exportDeptOrderStat(params).then(res => { |
| | | const blob = new Blob([res], { type: 'application/vnd.ms-excel' }); |
| | | const url = window.URL.createObjectURL(blob); |
| | | const a = document.createElement('a'); |
| | | a.href = url; |
| | | a.download = `分公司录单统计_${params.startDate}_${params.endDate}.xlsx`; |
| | | a.click(); |
| | | window.URL.revokeObjectURL(url); |
| | | this.exportLoading = false; |
| | | }).catch(() => { |
| | | this.exportLoading = false; |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | |
| | | |
| | | const baseUrl = 'http://localhost:8080' // 后端接口 |
| | | |
| | | const port = process.env.port || process.env.npm_config_port || 80 // 端口 |
| | | const port = process.env.port || process.env.npm_config_port || 8086 // 端口 |
| | | |
| | | // vue.config.js 配置说明 |
| | | //官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions |
| New file |
| | |
| | | -- ---------------------------- |
| | | -- 分公司录单统计 菜单权限配置 |
| | | -- 挂载在已有的"任务管理"目录下 |
| | | -- ---------------------------- |
| | | |
| | | -- 获取"任务管理"目录的菜单ID |
| | | SET @taskParentId = (SELECT menu_id FROM sys_menu WHERE menu_name = '任务管理' AND menu_type = 'M' LIMIT 1); |
| | | |
| | | -- 分公司录单统计 菜单(子菜单) |
| | | INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) |
| | | VALUES ('分公司录单统计', @taskParentId, 9, 'stat/index', 'task/stat/index', 1, 0, 'C', '0', '0', 'task:stat:query', 'chart', 'admin', SYSDATE(), '', NULL, '分公司录单统计菜单'); |
| | | |
| | | -- 获取刚插入的菜单ID |
| | | SET @statMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '分公司录单统计' AND menu_type = 'C' LIMIT 1); |
| | | |
| | | -- 按钮权限 |
| | | INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) |
| | | VALUES |
| | | ('录单统计查询', @statMenuId, 1, '#', '', 1, 0, 'F', '0', '0', 'task:stat:query', '#', 'admin', SYSDATE(), '', NULL, ''), |
| | | ('录单统计导出', @statMenuId, 2, '#', '', 1, 0, 'F', '0', '0', 'task:stat:export', '#', 'admin', SYSDATE(), '', NULL, ''); |
| New file |
| | |
| | | -- ===================================================== |
| | | -- 车辆行驶轨迹(天地图)菜单配置SQL |
| | | -- 说明:在系统管理菜单中添加"车辆行驶轨迹"菜单项 |
| | | -- 前端路由:/system/gps/trackMap |
| | | -- 组件路径:system/gps/trackMap |
| | | -- 所需权限:system:gps:list |
| | | -- 执行方式:mysql -u root -p 数据库名 < vehicle_gps_track_map_menu.sql |
| | | -- ===================================================== |
| | | |
| | | -- 添加"车辆行驶轨迹"菜单(父菜单ID=3,即车辆管理目录) |
| | | -- parent_id 根据实际数据库中的车辆管理目录ID调整 |
| | | INSERT INTO sys_menu ( |
| | | menu_name, |
| | | parent_id, |
| | | order_num, |
| | | path, |
| | | component, |
| | | is_frame, |
| | | is_cache, |
| | | menu_type, |
| | | visible, |
| | | status, |
| | | perms, |
| | | icon, |
| | | create_by, |
| | | create_time, |
| | | update_by, |
| | | update_time, |
| | | remark |
| | | ) VALUES ( |
| | | '车辆行驶轨迹', |
| | | 3, -- 车辆管理目录ID(如不对请修改为实际ID) |
| | | 10, |
| | | 'trackMap', |
| | | 'system/gps/trackMap', |
| | | 1, |
| | | 0, |
| | | 'C', |
| | | '0', |
| | | '0', |
| | | 'system:gps:list', |
| | | 'guide', |
| | | 'admin', |
| | | sysdate(), |
| | | '', |
| | | null, |
| | | '车辆行驶轨迹(天地图)查询页面' |
| | | ); |
| | | |
| | | -- 查询结果验证 |
| | | SELECT menu_id, menu_name, parent_id, path, component, perms, status |
| | | FROM sys_menu |
| | | WHERE menu_name = '车辆行驶轨迹'; |
| New file |
| | |
| | | <!DOCTYPE html> |
| | | <html> |
| | | <head> |
| | | <meta http-equiv="content-type" content="text/html; charset=utf-8"/> |
| | | <meta name="keywords" content="天地图"/> |
| | | <title>天地图-地图API-范例-经纬度直投地图</title> |
| | | <script type="text/javascript" src="http://api.tianditu.gov.cn/api?v=4.0&tk=079300b89bce333ead2df1476c43ecb0"></script> |
| | | <style type="text/css">body,html{width:100%;height:100%;margin:0;font-family:"Microsoft YaHei"}#mapDiv{width:100%;height:400px}input,b,p{margin-left:5px;font-size:14px}</style> |
| | | <script> |
| | | var map; |
| | | var zoom = 12; |
| | | function onLoad() { |
| | | map = new T.Map('mapDiv', { |
| | | projection: 'EPSG:4326' |
| | | }); |
| | | map.centerAndZoom(new T.LngLat(116.40769, 39.89945), zoom); |
| | | } |
| | | </script> |
| | | </head> |
| | | <body onLoad="onLoad()"> |
| | | <div id="mapDiv"></div> |
| | | <p>本示例演示如何显示经纬度直投地图。</p> |
| | | </body> |
| | | </html> |