| | |
| | | m.put("deptId", u.getDeptId()); |
| | | SysDept dept = u.getDeptId() == null ? null : sysDeptMapper.selectById(u.getDeptId()); |
| | | m.put("deptName", dept == null ? null : dept.getDeptName()); |
| | | m.put("deptLeaderFlag", Boolean.TRUE.equals(u.getDeptLeaderFlag())); |
| | | m.put("deptLeaderFlag", false); |
| | | m.put("nickName", u.getNickName()); |
| | | m.put("avatarUrl", u.getAvatarUrl()); |
| | | m.put("mobile", u.getMobile()); |
| | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | import java.util.List; |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | |
| | | @NotNull |
| | | @Schema(description = "1 启用 0 禁用") |
| | | private Integer status; |
| | | |
| | | @Schema(description = "部门负责人 ids") |
| | | private List<Long> leaderIds; |
| | | } |
| | |
| | | private String deptName; |
| | | private Integer sortOrder; |
| | | private Integer status; |
| | | private List<Long> leaderIds = new ArrayList<>(); |
| | | private String leaderNames; |
| | | |
| | | @Schema(description = "从根算起层级,根为 1,最大 5") |
| | | private int level; |
| | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | import java.util.List; |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | |
| | | |
| | | @Schema(description = "上级部门 id(仅非一级部门有效;不传则保持原上级)") |
| | | private Long parentId; |
| | | |
| | | @Schema(description = "部门负责人 ids") |
| | | private List<Long> leaderIds; |
| | | } |
| | |
| | | private String mobileMasked; |
| | | private Long deptId; |
| | | private String deptName; |
| | | private String deptPath; |
| | | private String positionLabel; |
| | | private String groupDisplay; |
| | | private Integer status; |
| | |
| | | @Schema(description = "排序") |
| | | private Integer sortOrder; |
| | | |
| | | @Schema(description = "部门负责人 ids,逗号分隔") |
| | | private String leaderIds; |
| | | |
| | | /** 1 启用 0 禁用 */ |
| | | @Schema(description = "部门状态:1启用 0禁用") |
| | | private Integer status; |
| | |
| | | import com.sinata.appointment.entity.MpPassLocalUnitMember; |
| | | import com.sinata.appointment.entity.MpStaffGroupMember; |
| | | import com.sinata.appointment.entity.MpUser; |
| | | import com.sinata.appointment.entity.SysDept; |
| | | import com.sinata.appointment.exception.BizException; |
| | | import com.sinata.appointment.mapper.ApprovalInstanceMapper; |
| | | import com.sinata.appointment.mapper.ApprovalTaskMapper; |
| | |
| | | import com.sinata.appointment.mapper.MpPassLocalUnitMemberMapper; |
| | | import com.sinata.appointment.mapper.MpStaffGroupMemberMapper; |
| | | import com.sinata.appointment.mapper.MpUserMapper; |
| | | import com.sinata.appointment.mapper.SysDeptMapper; |
| | | import com.sinata.appointment.constants.BizErrorCode; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | |
| | | private final ApprovalInstanceMapper approvalInstanceMapper; |
| | | private final ApprovalTaskMapper approvalTaskMapper; |
| | | private final MpUserMapper mpUserMapper; |
| | | private final SysDeptMapper sysDeptMapper; |
| | | private final MpPassLocalUnitMapper mpPassLocalUnitMapper; |
| | | private final MpPassLocalUnitMemberMapper mpPassLocalUnitMemberMapper; |
| | | private final MpStaffGroupMemberMapper mpStaffGroupMemberMapper; |
| | |
| | | } |
| | | if (ApprovalNodeCode.LEVEL2_DEPT_LEADER.equals(nodeCode)) { |
| | | Long deptId = resolveVisitedDeptId(order); |
| | | return listEmployeeIdsByDeptAndLeaderFlag(deptId, true); |
| | | return getDeptLeaderIds(deptId); |
| | | } |
| | | if (ApprovalNodeCode.LEVEL4_VISIT_DEPT.equals(nodeCode)) { |
| | | Long competentDeptId = getVisitCompetentDeptId(); |
| | |
| | | } |
| | | if (ApprovalNodeCode.LEVEL5_VISIT_DEPT_LEADER.equals(nodeCode)) { |
| | | Long competentDeptId = getVisitCompetentDeptId(); |
| | | return listEmployeeIdsByDeptAndLeaderFlag(competentDeptId, true); |
| | | return getDeptLeaderIds(competentDeptId); |
| | | } |
| | | if (ApprovalNodeCode.LEVEL3_LOCAL_DEPT_LEADER.equals(nodeCode)) { |
| | | return List.of(); |
| | |
| | | } |
| | | } |
| | | |
| | | private List<Long> listEmployeeIdsByDeptAndLeaderFlag(Long deptId, boolean leaderFlag) { |
| | | private List<Long> getDeptLeaderIds(Long deptId) { |
| | | if (deptId == null || deptId <= 0) { |
| | | return List.of(); |
| | | } |
| | | List<MpUser> users = |
| | | SysDept dept = sysDeptMapper.selectById(deptId); |
| | | if (dept == null || dept.getDelFlag() != null && dept.getDelFlag() == 1) { |
| | | return List.of(); |
| | | } |
| | | List<Long> leaderIds = parseDeptLeaderIds(dept); |
| | | if (leaderIds.isEmpty()) { |
| | | return List.of(); |
| | | } |
| | | List<MpUser> leaders = |
| | | mpUserMapper.selectList( |
| | | new LambdaQueryWrapper<MpUser>() |
| | | .eq(MpUser::getDelFlag, 0) |
| | | .eq(MpUser::getStatus, 1) |
| | | .eq(MpUser::getUserType, MiniUserType.EMPLOYEE) |
| | | .eq(MpUser::getDeptId, deptId) |
| | | .eq(MpUser::getDeptLeaderFlag, leaderFlag)); |
| | | return users.stream().map(MpUser::getId).filter(Objects::nonNull).toList(); |
| | | .in(MpUser::getId, leaderIds)); |
| | | Map<Long, MpUser> leaderMap = new LinkedHashMap<>(); |
| | | for (MpUser leader : leaders) { |
| | | leaderMap.put(leader.getId(), leader); |
| | | } |
| | | List<Long> result = new ArrayList<>(); |
| | | for (Long id : leaderIds) { |
| | | if (leaderMap.containsKey(id)) { |
| | | result.add(id); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<Long> parseDeptLeaderIds(SysDept dept) { |
| | | List<Long> ids = new ArrayList<>(); |
| | | Set<Long> seen = new LinkedHashSet<>(); |
| | | if (StringUtils.hasText(dept.getLeaderIds())) { |
| | | for (String part : dept.getLeaderIds().split(",")) { |
| | | if (!StringUtils.hasText(part)) { |
| | | continue; |
| | | } |
| | | try { |
| | | Long id = Long.parseLong(part.trim()); |
| | | if (id > 0 && seen.add(id)) { |
| | | ids.add(id); |
| | | } |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | } |
| | | } |
| | | return ids; |
| | | } |
| | | |
| | | private List<Long> listAllActiveEmployeeIdsByDept(Long deptId) { |
| | |
| | | } |
| | | |
| | | private List<Long> listEmployeeIdsByDeptWithLeaderFallback(Long deptId) { |
| | | List<Long> normalEmployees = listEmployeeIdsByDeptAndLeaderFlag(deptId, false); |
| | | if (normalEmployees != null && !normalEmployees.isEmpty()) { |
| | | return normalEmployees; |
| | | } |
| | | return listEmployeeIdsByDeptAndLeaderFlag(deptId, true); |
| | | return listAllActiveEmployeeIdsByDept(deptId); |
| | | } |
| | | |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.sinata.appointment.constants.BizErrorCode; |
| | | import com.sinata.appointment.constants.MiniUserType; |
| | | import com.sinata.appointment.dto.DeptCreateRequest; |
| | | import com.sinata.appointment.dto.DeptTreeNodeVo; |
| | | import com.sinata.appointment.dto.DeptUpdateRequest; |
| | |
| | | d.setDeptName(trimName(req.getDeptName())); |
| | | d.setStatus(normalizeStatus(req.getStatus())); |
| | | d.setSortOrder(0); |
| | | applyDeptLeaders(d, req.getLeaderIds()); |
| | | sysDeptMapper.insert(d); |
| | | SysDept fresh = sysDeptMapper.selectById(d.getId()); |
| | | if (fresh == null || !StringUtils.hasText(fresh.getUuid())) { |
| | |
| | | SysDept d = loadActiveWithUuid(id, req.getUuid()); |
| | | d.setDeptName(trimName(req.getDeptName())); |
| | | d.setStatus(normalizeStatus(req.getStatus())); |
| | | applyDeptLeaders(d, req.getLeaderIds()); |
| | | |
| | | Long oldParent = d.getParentId() == null ? 0L : d.getParentId(); |
| | | if (oldParent == 0L) { |
| | |
| | | cur = p; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private void applyDeptLeaders(SysDept dept, List<Long> leaderIds) { |
| | | List<Long> normalized = normalizeLeaderIds(leaderIds); |
| | | validateLeaders(normalized); |
| | | dept.setLeaderIds(joinIds(normalized)); |
| | | } |
| | | |
| | | private List<Long> normalizeLeaderIds(List<Long> leaderIds) { |
| | | List<Long> raw = leaderIds == null ? new ArrayList<>() : new ArrayList<>(leaderIds); |
| | | List<Long> out = new ArrayList<>(); |
| | | Set<Long> seen = new HashSet<>(); |
| | | for (Long id : raw) { |
| | | if (id == null || id <= 0 || !seen.add(id)) { |
| | | continue; |
| | | } |
| | | out.add(id); |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | private void validateLeaders(List<Long> leaderIds) { |
| | | if (leaderIds == null || leaderIds.isEmpty()) { |
| | | return; |
| | | } |
| | | List<MpUser> users = |
| | | mpUserMapper.selectList( |
| | | new LambdaQueryWrapper<MpUser>() |
| | | .eq(MpUser::getDelFlag, 0) |
| | | .eq(MpUser::getStatus, 1) |
| | | .eq(MpUser::getUserType, MiniUserType.EMPLOYEE) |
| | | .in(MpUser::getId, leaderIds)); |
| | | Set<Long> validIds = new HashSet<>(); |
| | | for (MpUser user : users) { |
| | | validIds.add(user.getId()); |
| | | } |
| | | for (Long id : leaderIds) { |
| | | if (!validIds.contains(id)) { |
| | | throw new BizException(BizErrorCode.RESOURCE_VERIFY_FAILED, "部门负责人不存在或已禁用: " + id); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private int subtreeHeight(long id, Map<Long, List<SysDept>> byParent) { |
| | |
| | | v.setDeptName(d.getDeptName()); |
| | | v.setSortOrder(d.getSortOrder()); |
| | | v.setStatus(d.getStatus() == null ? 1 : d.getStatus()); |
| | | List<Long> leaderIds = parseLeaderIds(d); |
| | | v.setLeaderIds(leaderIds); |
| | | v.setLeaderNames(resolveLeaderNames(leaderIds)); |
| | | return v; |
| | | } |
| | | |
| | | private List<Long> parseLeaderIds(SysDept dept) { |
| | | return parseIds(dept.getLeaderIds()); |
| | | } |
| | | |
| | | private List<Long> parseIds(String raw) { |
| | | if (!StringUtils.hasText(raw)) { |
| | | return List.of(); |
| | | } |
| | | List<Long> ids = new ArrayList<>(); |
| | | Set<Long> seen = new HashSet<>(); |
| | | for (String part : raw.split(",")) { |
| | | if (!StringUtils.hasText(part)) { |
| | | continue; |
| | | } |
| | | try { |
| | | Long id = Long.parseLong(part.trim()); |
| | | if (id > 0 && seen.add(id)) { |
| | | ids.add(id); |
| | | } |
| | | } catch (NumberFormatException ignored) { |
| | | } |
| | | } |
| | | return ids; |
| | | } |
| | | |
| | | private String joinIds(List<Long> ids) { |
| | | if (ids == null || ids.isEmpty()) { |
| | | return null; |
| | | } |
| | | return ids.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(",")); |
| | | } |
| | | |
| | | private String resolveLeaderNames(List<Long> leaderIds) { |
| | | if (leaderIds == null || leaderIds.isEmpty()) { |
| | | return null; |
| | | } |
| | | List<MpUser> users = |
| | | mpUserMapper.selectList( |
| | | new LambdaQueryWrapper<MpUser>() |
| | | .eq(MpUser::getDelFlag, 0) |
| | | .in(MpUser::getId, leaderIds)); |
| | | Map<Long, String> names = new HashMap<>(); |
| | | for (MpUser user : users) { |
| | | names.put(user.getId(), user.getNickName()); |
| | | } |
| | | List<String> ordered = new ArrayList<>(); |
| | | for (Long id : leaderIds) { |
| | | String name = names.get(id); |
| | | if (StringUtils.hasText(name)) { |
| | | ordered.add(name.trim()); |
| | | } |
| | | } |
| | | return ordered.isEmpty() ? null : String.join("、", ordered); |
| | | } |
| | | |
| | | private SysDept loadActive(long id) { |
| | | SysDept d = sysDeptMapper.selectById(id); |
| | | if (d == null || d.getDelFlag() != null && d.getDelFlag() == 1) { |
| | |
| | | validateApprover(req.getApproverType(), req.getApproverDeptIds(), req.getApproverGroupId(), req.getMemberIds()); |
| | | MpPassLocalUnit e = new MpPassLocalUnit(); |
| | | e.setUnitName(req.getUnitName().trim()); |
| | | e.setRemark(trimToNull(req.getRemark())); |
| | | e.setRemark(trimToEmpty(req.getRemark())); |
| | | e.setApproverType(req.getApproverType()); |
| | | applyApproverColumns(e, req.getApproverType(), req.getApproverDeptIds(), req.getApproverGroupId()); |
| | | mpPassLocalUnitMapper.insert(e); |
| | |
| | | validateApprover(req.getApproverType(), req.getApproverDeptIds(), req.getApproverGroupId(), req.getMemberIds()); |
| | | mpPassLocalUnitMemberMapper.deletePhysicalByUnitId(e.getId()); |
| | | e.setUnitName(req.getUnitName().trim()); |
| | | e.setRemark(trimToNull(req.getRemark())); |
| | | e.setRemark(trimToEmpty(req.getRemark())); |
| | | e.setApproverType(req.getApproverType()); |
| | | applyApproverColumns(e, req.getApproverType(), req.getApproverDeptIds(), req.getApproverGroupId()); |
| | | mpPassLocalUnitMapper.updateById(e); |
| | |
| | | String t = s.trim(); |
| | | return t.isEmpty() ? null : t; |
| | | } |
| | | |
| | | private static String trimToEmpty(String s) { |
| | | return s == null ? "" : s.trim(); |
| | | } |
| | | } |
| | |
| | | SysDept dept = sysDeptMapper.selectById(user.getDeptId()); |
| | | deptName = dept == null ? null : dept.getDeptName(); |
| | | } |
| | | Boolean leader = |
| | | MiniUserType.EMPLOYEE.equals(user.getUserType()) |
| | | ? Boolean.TRUE.equals(user.getDeptLeaderFlag()) |
| | | : null; |
| | | return MiniLoginResponse.builder() |
| | | .token(MiniStpUtil.getTokenValue()) |
| | | .mpUserId(user.getId()) |
| | | .userType(user.getUserType()) |
| | | .deptId(user.getDeptId()) |
| | | .deptName(deptName) |
| | | .deptLeader(leader) |
| | | .deptLeader(MiniUserType.EMPLOYEE.equals(user.getUserType()) ? false : null) |
| | | .avatarUrl(user.getAvatarUrl()) |
| | | .companyName(user.getCompanyName()) |
| | | .displayName(name) |
| | |
| | | import com.sinata.appointment.mapper.MpUserMapper; |
| | | import com.sinata.appointment.mapper.SysDeptMapper; |
| | | import java.util.ArrayList; |
| | | import java.util.Comparator; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | new LambdaQueryWrapper<MpPassAccessArea>() |
| | | .eq(MpPassAccessArea::getDelFlag, 0) |
| | | .orderByAsc(MpPassAccessArea::getPid) |
| | | .orderByAsc(MpPassAccessArea::getAreaName) |
| | | .orderByAsc(MpPassAccessArea::getId)); |
| | | if (rows.isEmpty()) { |
| | | return List.of(); |
| | |
| | | } |
| | | parent.getChildren().add(current); |
| | | } |
| | | sortAreaTree(roots); |
| | | return roots; |
| | | } |
| | | |
| | | private void sortAreaTree(List<MiniPassAccessAreaTreeVo> nodes) { |
| | | nodes.sort( |
| | | Comparator.comparing( |
| | | MiniPassAccessAreaTreeVo::getAreaName, |
| | | Comparator.nullsLast(String::compareTo)) |
| | | .thenComparing(MiniPassAccessAreaTreeVo::getId)); |
| | | for (MiniPassAccessAreaTreeVo node : nodes) { |
| | | sortAreaTree(node.getChildren()); |
| | | } |
| | | } |
| | | |
| | | public List<MiniVisitedUserVo> searchVisitedUsersByExactName(String realName) { |
| | | String name = realName == null ? "" : realName.trim(); |
| | | if (name.isEmpty()) { |
| | |
| | | .eq(MpPassAccessArea::getDelFlag, 0) |
| | | .eq(MpPassAccessArea::getPid, p) |
| | | .like(StringUtils.hasText(areaName), MpPassAccessArea::getAreaName, trim(areaName)) |
| | | .orderByDesc(MpPassAccessArea::getCreateTime); |
| | | .orderByAsc(MpPassAccessArea::getAreaName) |
| | | .orderByAsc(MpPassAccessArea::getId); |
| | | Page<MpPassAccessArea> pg = mpPassAccessAreaMapper.selectPage(new Page<>(page, size), q); |
| | | return pg.convert(this::toListVo); |
| | | } |
| | |
| | | MpPassAccessArea e = new MpPassAccessArea(); |
| | | e.setPid(pid); |
| | | e.setAreaName(name); |
| | | e.setRemark(trimToNull(req.getRemark())); |
| | | e.setAreaDesc(trimToNull(req.getAreaDesc())); |
| | | e.setRemark(trimToEmpty(req.getRemark())); |
| | | e.setAreaDesc(trimToEmpty(req.getAreaDesc())); |
| | | mpPassAccessAreaMapper.insert(e); |
| | | MpPassAccessArea fresh = mpPassAccessAreaMapper.selectById(e.getId()); |
| | | if (fresh == null || !StringUtils.hasText(fresh.getUuid())) { |
| | |
| | | e.setPid(targetPid); |
| | | e.setAreaName(name); |
| | | if (req.getRemark() != null) { |
| | | e.setRemark(trimToNull(req.getRemark())); |
| | | e.setRemark(trimToEmpty(req.getRemark())); |
| | | } |
| | | e.setAreaDesc(trimToNull(req.getAreaDesc())); |
| | | e.setAreaDesc(trimToEmpty(req.getAreaDesc())); |
| | | mpPassAccessAreaMapper.updateById(e); |
| | | evictMiniPassAccessAreaTreeCache(); |
| | | } |
| | |
| | | String t = s.trim(); |
| | | return t.isEmpty() ? null : t; |
| | | } |
| | | |
| | | private static String trimToEmpty(String s) { |
| | | return s == null ? "" : s.trim(); |
| | | } |
| | | } |
| | |
| | | q.like(StringUtils.hasText(staffNo), MpUser::getStaffNo, staffNo != null ? staffNo.trim() : null); |
| | | q.like(StringUtils.hasText(mobile), MpUser::getMobile, mobile != null ? mobile.trim() : null); |
| | | if ("LEADER".equalsIgnoreCase(position)) { |
| | | q.eq(MpUser::getDeptLeaderFlag, true); |
| | | q.apply("1 = 0"); |
| | | } else if ("STAFF".equalsIgnoreCase(position)) { |
| | | q.and( |
| | | w -> |
| | |
| | | } |
| | | userIds.add(u.getId()); |
| | | } |
| | | List<SysDept> allDepts = sysDeptMapper.selectList( |
| | | new LambdaQueryWrapper<SysDept>().eq(SysDept::getDelFlag, 0)); |
| | | Map<Long, String> deptPaths = buildDeptPathById(allDepts); |
| | | Map<Long, String> deptNames = deptNamesByIds(deptIds); |
| | | Map<Long, String> groupDisplay = groupDisplayByMpUserIds(userIds); |
| | | return p.convert(u -> toListVo(u, deptNames, groupDisplay.getOrDefault(u.getId(), "—"))); |
| | | return p.convert(u -> toListVo(u, deptNames, deptPaths, groupDisplay.getOrDefault(u.getId(), "—"))); |
| | | } |
| | | |
| | | private Set<Long> mpUserIdsInStaffGroup(Long staffGroupId) { |
| | |
| | | u.setMobile(mobileRaw); |
| | | u.setAvatarUrl(trimToNull(req.getAvatarUrl())); |
| | | u.setDeptId(req.getDeptId()); |
| | | u.setDeptLeaderFlag(Boolean.TRUE.equals(req.getDeptLeader())); |
| | | u.setDeptLeaderFlag(false); |
| | | u.setStatus(1); |
| | | mpUserMapper.insert(u); |
| | | MpUser fresh = mpUserMapper.selectById(u.getId()); |
| | |
| | | u.setAvatarUrl(trimToNull(req.getAvatarUrl())); |
| | | u.setMobile(mobileRaw); |
| | | u.setDeptId(req.getDeptId()); |
| | | u.setDeptLeaderFlag(Boolean.TRUE.equals(req.getDeptLeader())); |
| | | u.setDeptLeaderFlag(false); |
| | | mpUserMapper.updateById(u); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | Long deptId = parseDeptId(dept, deptPathToId, deptNameToIds); |
| | | boolean leader = parseDeptLeader(position); |
| | | parseDeptLeader(position); |
| | | |
| | | StaffCreateRequest req = new StaffCreateRequest(); |
| | | req.setNickName(name); |
| | | req.setStaffNo(StringUtils.hasText(staffNo) ? staffNo : null); |
| | | req.setMobile(mobile); |
| | | req.setDeptId(deptId); |
| | | req.setDeptLeader(leader); |
| | | req.setDeptLeader(false); |
| | | Map<String, Object> created = create(req); |
| | | Object idObj = created.get("id"); |
| | | Long uid = idObj instanceof Number ? ((Number) idObj).longValue() : null; |
| | |
| | | List<List<String>> rows = new ArrayList<>(); |
| | | for (int i = 0; i < max; i++) { |
| | | String dept = i < deptPaths.size() ? deptPaths.get(i) : ""; |
| | | String pos = i == 0 ? "部门负责人" : (i == 1 ? "员工" : ""); |
| | | String pos = i == 0 ? "员工" : ""; |
| | | String grp = i < groupNames.size() ? groupNames.get(i) : ""; |
| | | rows.add(java.util.Arrays.asList(dept, pos, grp)); |
| | | } |
| | |
| | | } |
| | | Name posRange = wb.createName(); |
| | | posRange.setNameName("pos_list"); |
| | | posRange.setRefersToFormula("字典!$B$1:$B$2"); |
| | | posRange.setRefersToFormula("字典!$B$1:$B$1"); |
| | | if (!groupNames.isEmpty()) { |
| | | Name groupRange = wb.createName(); |
| | | groupRange.setNameName("group_list"); |
| | |
| | | return false; |
| | | } |
| | | String t = position.trim(); |
| | | if ("部门负责人".equals(t) || "LEADER".equalsIgnoreCase(t)) { |
| | | return true; |
| | | } |
| | | if ("员工".equals(t) || "STAFF".equalsIgnoreCase(t)) { |
| | | return false; |
| | | } |
| | |
| | | exist.setAvatarUrl(trimToNull(req.getAvatarUrl())); |
| | | exist.setMobile(mobileRaw); |
| | | exist.setDeptId(req.getDeptId()); |
| | | exist.setDeptLeaderFlag(Boolean.TRUE.equals(req.getDeptLeader())); |
| | | exist.setDeptLeaderFlag(false); |
| | | exist.setStatus(1); |
| | | mpUserMapper.updateById(exist); |
| | | } |
| | |
| | | return m; |
| | | } |
| | | |
| | | private StaffListVo toListVo(MpUser u, Map<Long, String> deptNames, String groupDisplay) { |
| | | private StaffListVo toListVo(MpUser u, Map<Long, String> deptNames, Map<Long, String> deptPaths, String groupDisplay) { |
| | | StaffListVo vo = new StaffListVo(); |
| | | vo.setId(u.getId()); |
| | | vo.setUuid(u.getUuid()); |
| | |
| | | vo.setDeptId(u.getDeptId()); |
| | | vo.setDeptName( |
| | | u.getDeptId() == null ? "—" : deptNames.getOrDefault(u.getDeptId(), "—")); |
| | | vo.setPositionLabel(Boolean.TRUE.equals(u.getDeptLeaderFlag()) ? "部门负责人" : "员工"); |
| | | vo.setDeptPath( |
| | | u.getDeptId() == null ? "—" : deptPaths.getOrDefault(u.getDeptId(), "—")); |
| | | vo.setPositionLabel("员工"); |
| | | vo.setGroupDisplay(groupDisplay); |
| | | vo.setStatus(u.getStatus() == null ? 1 : u.getStatus()); |
| | | vo.setCreateTime(u.getCreateTime()); |
| | |
| | | vo.setAvatarUrl(u.getAvatarUrl()); |
| | | vo.setMobile(u.getMobile()); |
| | | vo.setDeptId(u.getDeptId()); |
| | | vo.setDeptLeader(Boolean.TRUE.equals(u.getDeptLeaderFlag())); |
| | | vo.setDeptLeader(false); |
| | | vo.setStatus(u.getStatus() == null ? 1 : u.getStatus()); |
| | | vo.setGroupNames(loadGroupNamesForUser(u.getId())); |
| | | return vo; |
| | |
| | | SysRole r = new SysRole(); |
| | | r.setRoleCode(code); |
| | | r.setRoleName(roleName); |
| | | r.setRemark(trimToNull(req.getRemark())); |
| | | r.setRemark(trimToEmpty(req.getRemark())); |
| | | sysRoleMapper.insert(r); |
| | | SysRole fresh = sysRoleMapper.selectById(r.getId()); |
| | | if (fresh == null || !StringUtils.hasText(fresh.getUuid())) { |
| | |
| | | String roleName = req.getRoleName().trim(); |
| | | validateRoleNameUnique(roleName, id); |
| | | r.setRoleName(roleName); |
| | | r.setRemark(trimToNull(req.getRemark())); |
| | | r.setRemark(trimToEmpty(req.getRemark())); |
| | | sysRoleMapper.updateById(r); |
| | | replaceRoleMenus(id, req.getMenuIds()); |
| | | } |
| | |
| | | String t = s.trim(); |
| | | return t.isEmpty() ? null : t; |
| | | } |
| | | |
| | | private static String trimToEmpty(String s) { |
| | | return s == null ? "" : s.trim(); |
| | | } |
| | | } |
| New file |
| | |
| | | ALTER TABLE sys_dept |
| | | ADD COLUMN leader_ids VARCHAR(512) NULL COMMENT '部门负责人ids,逗号分隔' AFTER sort_order; |
| New file |
| | |
| | | /** |
| | | * 处理表单提交数据 |
| | | * 将字符串类型的 null/undefined 转换为空字符串 |
| | | * |
| | | * @param data 表单数据对象 |
| | | * @returns 处理后的数据对象 |
| | | */ |
| | | export function normalizeFormData<T extends Record<string, any>>(data: T): T { |
| | | const result: any = {}; |
| | | |
| | | for (const key in data) { |
| | | const value = data[key]; |
| | | |
| | | // 字符串类型:null/undefined 转为空字符串 |
| | | if (typeof value === 'string' || value == null) { |
| | | result[key] = value || ''; |
| | | } else { |
| | | result[key] = value; |
| | | } |
| | | } |
| | | |
| | | return result as T; |
| | | } |
| | |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | const UI_NODE_CODES = [ |
| | | "LEVEL2_DEPT_LEADER", |
| | |
| | | } |
| | | savingDept.value = true; |
| | | try { |
| | | await http.put("/config/visitor-competent-dept", { |
| | | const payload = normalizeFormData({ |
| | | deptId: competentDeptId.value ?? null, |
| | | }); |
| | | await http.put("/config/visitor-competent-dept", payload); |
| | | ElMessage.success("已保存"); |
| | | } catch (e: unknown) { |
| | | ElMessage.error(e instanceof Error ? e.message : "保存失败"); |
| | |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | const LOCKED_CODES = new Set(["respondent", "visit_start_time"]); |
| | | |
| | |
| | | } |
| | | extrasSaving.value = true; |
| | | try { |
| | | await http.put("/config/booking-settings", { |
| | | const payload = normalizeFormData({ |
| | | rangeMode: rangeMode.value, |
| | | maxConsecutiveDays: rangeMode.value === "MULTI" ? maxConsecutiveDays.value : maxConsecutiveDays.value ?? 7, |
| | | holidayDates: holidayDates.value, |
| | | vehicleBarrierEnabled: vehicleBarrierEnabled.value, |
| | | }); |
| | | await http.put("/config/booking-settings", payload); |
| | | ElMessage.success("已保存"); |
| | | await loadExtras(); |
| | | } catch (e: unknown) { |
| | |
| | | import { ElMessage } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import type { IEditorConfig } from "@wangeditor/editor"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type TabKey = "ABOUT" | "NOTICE" | "FLOW"; |
| | | |
| | |
| | | async function save() { |
| | | saving.value = true; |
| | | try { |
| | | await http.put("/config/content-settings", { |
| | | const payload = normalizeFormData({ |
| | | aboutUsHtml: form.value.aboutUsHtml, |
| | | parkNoticeHtml: form.value.parkNoticeHtml, |
| | | parkFlowHtml: form.value.parkFlowHtml, |
| | | aboutUsEnabled: form.value.aboutUsEnabled, |
| | | }); |
| | | await http.put("/config/content-settings", payload); |
| | | await load(); |
| | | ElMessage.success("已保存"); |
| | | } catch (e: unknown) { |
| | |
| | | <el-form-item label="部门名称" required> |
| | | <el-input v-model="form.deptName" maxlength="128" placeholder="请输入" /> |
| | | </el-form-item> |
| | | <el-form-item label="部门负责人"> |
| | | <div class="leader-picker"> |
| | | <el-button @click="openLeaderPicker">选择负责人</el-button> |
| | | <el-button v-if="selectedLeaders.length" link type="primary" @click="clearLeaders">清空</el-button> |
| | | <div v-if="selectedLeaders.length" class="leader-tags"> |
| | | <el-tag |
| | | v-for="leader in selectedLeaders" |
| | | :key="leader.id" |
| | | closable |
| | | @close="removeLeader(leader.id)" |
| | | > |
| | | {{ leader.realName }} |
| | | </el-tag> |
| | | </div> |
| | | <div v-else class="leader-empty">未选择负责人</div> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="部门状态" required> |
| | | <el-radio-group v-model="form.status"> |
| | | <el-radio :label="1">启用</el-radio> |
| | |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="leaderDlg" title="选择部门负责人" width="760px" destroy-on-close @closed="resetLeaderDialog"> |
| | | <el-form class="leader-search" @submit.prevent="loadLeaderCandidates"> |
| | | <el-form-item label="姓名"> |
| | | <el-input v-model="leaderFilters.realName" placeholder="请输入姓名" clearable /> |
| | | </el-form-item> |
| | | <el-form-item label="联系方式"> |
| | | <el-input v-model="leaderFilters.mobile" placeholder="请输入联系方式" clearable /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button @click="resetLeaderSearch">重置</el-button> |
| | | <el-button type="primary" native-type="submit">搜索</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-table |
| | | ref="leaderTableRef" |
| | | :data="leaderRows" |
| | | v-loading="leaderLoading" |
| | | row-key="id" |
| | | border |
| | | height="360" |
| | | @selection-change="onLeaderSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="48" reserve-selection /> |
| | | <el-table-column prop="realName" label="姓名" min-width="120" show-overflow-tooltip /> |
| | | <el-table-column prop="mobileMasked" label="联系方式" width="140" /> |
| | | <el-table-column prop="deptPath" label="所属部门" min-width="220" show-overflow-tooltip /> |
| | | </el-table> |
| | | <div class="leader-pager"> |
| | | <div>已选 {{ leaderSelection.length }} 人</div> |
| | | <el-pagination |
| | | background |
| | | layout="sizes, prev, pager, next" |
| | | :total="leaderTotal" |
| | | :page-size="leaderSize" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | v-model:current-page="leaderPage" |
| | | @current-change="loadLeaderCandidates" |
| | | @size-change="onLeaderSizeChange" |
| | | /> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="leaderDlg = false">取消</el-button> |
| | | <el-button type="primary" @click="confirmLeaders">确定</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed, ref } from "vue"; |
| | | import { computed, nextTick, ref } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type DeptNode = { |
| | | id: number; |
| | |
| | | sortOrder?: number; |
| | | status: number; |
| | | level: number; |
| | | leaderIds?: number[]; |
| | | leaderNames?: string; |
| | | children?: DeptNode[]; |
| | | }; |
| | | |
| | |
| | | const parentDeptName = ref(""); |
| | | const childParentId = ref<number | null>(null); |
| | | const pendingExpandRowKey = ref<string | null>(null); |
| | | type StaffOption = { id: number; realName: string; deptPath: string; mobileMasked?: string }; |
| | | const staffOptions = ref<StaffOption[]>([]); |
| | | const selectedLeaders = ref<StaffOption[]>([]); |
| | | const leaderDlg = ref(false); |
| | | const leaderTableRef = ref(); |
| | | const leaderLoading = ref(false); |
| | | const leaderRows = ref<StaffOption[]>([]); |
| | | const leaderSelection = ref<StaffOption[]>([]); |
| | | const leaderPage = ref(1); |
| | | const leaderSize = ref(10); |
| | | const leaderTotal = ref(0); |
| | | const leaderFilters = ref({ realName: "", mobile: "" }); |
| | | |
| | | const form = ref({ |
| | | uuid: "", |
| | | deptName: "", |
| | | status: 1, |
| | | parentId: undefined as number | undefined, |
| | | leaderIds: [] as number[], |
| | | }); |
| | | |
| | | const dlgTitle = computed(() => { |
| | |
| | | expandedRowKeys.value = expandedRowKeys.value.filter(id => !collapsedKeys.has(id)); |
| | | } |
| | | |
| | | async function loadStaffOptionsByIds(ids: number[]) { |
| | | const missingIds = ids.filter(id => !staffOptions.value.some(s => s.id === id)); |
| | | if (!missingIds.length) return; |
| | | try { |
| | | const { data } = await http.get("/sys/staff", { params: { page: 1, size: 1000, status: 1 } }); |
| | | const staffList = (data.result || []) as { id: number; realName: string; deptPath?: string }[]; |
| | | staffOptions.value = staffList.map(s => ({ |
| | | id: s.id, |
| | | realName: s.realName, |
| | | deptPath: s.deptPath || '未分配部门', |
| | | })); |
| | | } catch (e: unknown) { |
| | | console.error("加载员工列表失败", e); |
| | | } |
| | | } |
| | | |
| | | function syncSelectedLeadersFromIds(ids: number[], namesText?: string) { |
| | | const names = (namesText || "").split("、").filter(Boolean); |
| | | selectedLeaders.value = ids.map((id, index) => { |
| | | const cached = staffOptions.value.find(s => s.id === id); |
| | | return cached || { id, realName: names[index] || `员工${id}`, deptPath: "" }; |
| | | }); |
| | | } |
| | | |
| | | function openAddRoot() { |
| | | mode.value = "root"; |
| | | editingId.value = null; |
| | | editingIsRoot.value = true; |
| | | parentDeptName.value = ""; |
| | | childParentId.value = null; |
| | | form.value = { uuid: "", deptName: "", status: 1, parentId: undefined }; |
| | | form.value = { uuid: "", deptName: "", status: 1, parentId: undefined, leaderIds: [] }; |
| | | selectedLeaders.value = []; |
| | | dlg.value = true; |
| | | } |
| | | |
| | |
| | | editingIsRoot.value = false; |
| | | parentDeptName.value = row.deptName; |
| | | childParentId.value = row.id; |
| | | form.value = { uuid: "", deptName: "", status: 1, parentId: row.id }; |
| | | form.value = { uuid: "", deptName: "", status: 1, parentId: row.id, leaderIds: [] }; |
| | | selectedLeaders.value = []; |
| | | dlg.value = true; |
| | | } |
| | | |
| | | function openEdit(row: DeptNode) { |
| | | async function openEdit(row: DeptNode) { |
| | | mode.value = "edit"; |
| | | editingId.value = row.id; |
| | | const root = !row.parentId || row.parentId === 0; |
| | | editingIsRoot.value = root; |
| | | parentDeptName.value = ""; |
| | | childParentId.value = null; |
| | | const leaderIds = Array.isArray(row.leaderIds) ? row.leaderIds : []; |
| | | await loadStaffOptionsByIds(leaderIds); |
| | | form.value = { |
| | | uuid: row.uuid, |
| | | deptName: row.deptName, |
| | | status: row.status, |
| | | parentId: root ? undefined : row.parentId, |
| | | leaderIds, |
| | | }; |
| | | syncSelectedLeadersFromIds(leaderIds, row.leaderNames); |
| | | if (!root) { |
| | | const disabled = new Set(collectDescendantIds(row)); |
| | | parentTreeSelectData.value = toTreeSelectData(treeData.value, disabled); |
| | |
| | | parentTreeSelectData.value = []; |
| | | } |
| | | |
| | | function openLeaderPicker() { |
| | | leaderSelection.value = [...selectedLeaders.value]; |
| | | leaderDlg.value = true; |
| | | loadLeaderCandidates(); |
| | | } |
| | | |
| | | async function loadLeaderCandidates() { |
| | | leaderLoading.value = true; |
| | | try { |
| | | const { data } = await http.get("/sys/staff", { |
| | | params: { |
| | | page: leaderPage.value, |
| | | size: leaderSize.value, |
| | | status: 1, |
| | | realName: leaderFilters.value.realName.trim() || undefined, |
| | | mobile: leaderFilters.value.mobile.trim() || undefined, |
| | | }, |
| | | }); |
| | | leaderRows.value = ((data.result || []) as StaffOption[]).map(s => ({ |
| | | ...s, |
| | | deptPath: s.deptPath || "未分配部门", |
| | | })); |
| | | leaderTotal.value = Number(data.total) || 0; |
| | | await nextTick(); |
| | | syncLeaderTableSelection(); |
| | | } catch (e: unknown) { |
| | | ElMessage.error(e instanceof Error ? e.message : "加载员工失败"); |
| | | } finally { |
| | | leaderLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function syncLeaderTableSelection() { |
| | | const selectedIds = new Set(leaderSelection.value.map(s => s.id)); |
| | | leaderRows.value.forEach(row => { |
| | | leaderTableRef.value?.toggleRowSelection(row, selectedIds.has(row.id)); |
| | | }); |
| | | } |
| | | |
| | | function onLeaderSelectionChange(selection: StaffOption[]) { |
| | | const currentPageIds = new Set(leaderRows.value.map(s => s.id)); |
| | | const kept = leaderSelection.value.filter(s => !currentPageIds.has(s.id)); |
| | | const merged = [...kept, ...selection]; |
| | | const seen = new Set<number>(); |
| | | leaderSelection.value = merged.filter(item => { |
| | | if (seen.has(item.id)) return false; |
| | | seen.add(item.id); |
| | | return true; |
| | | }); |
| | | } |
| | | |
| | | function resetLeaderSearch() { |
| | | leaderFilters.value = { realName: "", mobile: "" }; |
| | | leaderPage.value = 1; |
| | | loadLeaderCandidates(); |
| | | } |
| | | |
| | | function onLeaderSizeChange(v: number) { |
| | | leaderSize.value = v; |
| | | leaderPage.value = 1; |
| | | loadLeaderCandidates(); |
| | | } |
| | | |
| | | function confirmLeaders() { |
| | | selectedLeaders.value = [...leaderSelection.value]; |
| | | form.value.leaderIds = selectedLeaders.value.map(s => s.id); |
| | | leaderDlg.value = false; |
| | | } |
| | | |
| | | function resetLeaderDialog() { |
| | | leaderRows.value = []; |
| | | } |
| | | |
| | | function removeLeader(id: number) { |
| | | selectedLeaders.value = selectedLeaders.value.filter(s => s.id !== id); |
| | | form.value.leaderIds = selectedLeaders.value.map(s => s.id); |
| | | } |
| | | |
| | | function clearLeaders() { |
| | | selectedLeaders.value = []; |
| | | form.value.leaderIds = []; |
| | | } |
| | | |
| | | async function submitDlg() { |
| | | if (!form.value.deptName.trim()) { |
| | | ElMessage.warning("请填写部门名称"); |
| | |
| | | saving.value = true; |
| | | try { |
| | | if (mode.value === "root") { |
| | | await http.post("/sys/dept-manage", { |
| | | const payload = normalizeFormData({ |
| | | parentId: 0, |
| | | deptName: form.value.deptName.trim(), |
| | | status: form.value.status, |
| | | leaderIds: form.value.leaderIds, |
| | | }); |
| | | await http.post("/sys/dept-manage", payload); |
| | | ElMessage.success("已创建"); |
| | | } else if (mode.value === "child") { |
| | | if (childParentId.value == null) { |
| | |
| | | return; |
| | | } |
| | | pendingExpandRowKey.value = toRowKey(childParentId.value); |
| | | await http.post("/sys/dept-manage", { |
| | | const payload = normalizeFormData({ |
| | | parentId: childParentId.value, |
| | | deptName: form.value.deptName.trim(), |
| | | status: form.value.status, |
| | | leaderIds: form.value.leaderIds, |
| | | }); |
| | | await http.post("/sys/dept-manage", payload); |
| | | ElMessage.success("已创建"); |
| | | } else if (editingId.value != null) { |
| | | const body: Record<string, unknown> = { |
| | | uuid: form.value.uuid, |
| | | deptName: form.value.deptName.trim(), |
| | | status: form.value.status, |
| | | leaderIds: form.value.leaderIds, |
| | | }; |
| | | if (!editingIsRoot.value) { |
| | | if (form.value.parentId == null) { |
| | |
| | | } |
| | | body.parentId = form.value.parentId; |
| | | } |
| | | await http.put(`/sys/dept-manage/${editingId.value}`, body); |
| | | const payload = normalizeFormData(body); |
| | | await http.put(`/sys/dept-manage/${editingId.value}`, payload); |
| | | ElMessage.success("已保存"); |
| | | } |
| | | dlg.value = false; |
| | |
| | | width: 100%; |
| | | } |
| | | |
| | | .leader-picker { |
| | | width: 100%; |
| | | } |
| | | |
| | | .leader-tags { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .leader-empty { |
| | | color: #909399; |
| | | font-size: 13px; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .leader-search { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr auto; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .leader-pager { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-top: 12px; |
| | | } |
| | | |
| | | :deep(.table-ops .el-button) { |
| | | font-size: 14px; |
| | | } |
| | |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type Row = { |
| | | id: number; |
| | |
| | | saving.value = true; |
| | | try { |
| | | if (isCreate.value) { |
| | | const body: { pid: number; areaName: string; areaDesc?: string } = { |
| | | const body = normalizeFormData({ |
| | | pid: listPid.value > 0 ? listPid.value : 0, |
| | | areaName: form.areaName.trim(), |
| | | areaDesc: form.areaDesc.trim() || undefined, |
| | | }; |
| | | areaDesc: form.areaDesc.trim(), |
| | | }); |
| | | await http.post("/sys/pass-access-areas", body); |
| | | ElMessage.success("已添加"); |
| | | } else if (editingId.value != null) { |
| | | await http.put(`/sys/pass-access-areas/${editingId.value}`, { |
| | | const body = normalizeFormData({ |
| | | uuid: form.uuid, |
| | | pid: form.pid > 0 ? form.pid : 0, |
| | | areaName: form.areaName.trim(), |
| | | areaDesc: form.areaDesc.trim() || undefined, |
| | | areaDesc: form.areaDesc.trim(), |
| | | }); |
| | | await http.put(`/sys/pass-access-areas/${editingId.value}`, body); |
| | | ElMessage.success("已保存"); |
| | | } |
| | | dlg.value = false; |
| | |
| | | import { computed, nextTick, onMounted, reactive, ref } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type UnitRow = { |
| | | id: number; |
| | |
| | | const t = form.approverType; |
| | | const base: Record<string, unknown> = { |
| | | unitName: form.unitName.trim(), |
| | | remark: form.remark?.trim() || undefined, |
| | | remark: form.remark?.trim() || "", |
| | | approverType: t, |
| | | }; |
| | | if (t === 1) { |
| | |
| | | if (!validateForm()) return; |
| | | saving.value = true; |
| | | try { |
| | | const body = buildSubmitBody(); |
| | | const body = normalizeFormData(buildSubmitBody()); |
| | | if (!editing.value) { |
| | | await http.post("/sys/local-units", body); |
| | | ElMessage.success("已创建"); |
| | |
| | | import RolePermissionMatrix from "../components/RolePermissionMatrix.vue"; |
| | | import type { MenuNode } from "../types/sysMenu"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | |
| | | saving.value = true; |
| | | try { |
| | | if (isCreate.value) { |
| | | await http.post("/sys/role-manage", { |
| | | const payload = normalizeFormData({ |
| | | roleName: form.roleName.trim(), |
| | | remark: form.remark.trim() || undefined, |
| | | remark: form.remark.trim(), |
| | | menuIds: submitMenuIds, |
| | | }); |
| | | await http.post("/sys/role-manage", payload); |
| | | ElMessage.success("已创建"); |
| | | } else { |
| | | const uuid = route.query.uuid as string; |
| | | await http.put(`/sys/role-manage/${roleId.value}`, { |
| | | const payload = normalizeFormData({ |
| | | uuid, |
| | | roleName: form.roleName.trim(), |
| | | remark: form.remark.trim() || undefined, |
| | | remark: form.remark.trim(), |
| | | menuIds: submitMenuIds, |
| | | }); |
| | | await http.put(`/sys/role-manage/${roleId.value}`, payload); |
| | | ElMessage.success("已保存"); |
| | | } |
| | | router.push("/roles"); |
| | |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import type { UploadRequestOptions } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type Row = { |
| | | id: number; |
| | |
| | | |
| | | saving.value = true; |
| | | try { |
| | | const payload = normalizeFormData({ word }); |
| | | if (editingId.value == null) { |
| | | await http.post("/config/sensitive-words/manage", { word }); |
| | | await http.post("/config/sensitive-words/manage", payload); |
| | | ElMessage.success("已新增"); |
| | | page.value = 1; |
| | | } else { |
| | | await http.put(`/config/sensitive-words/manage/${editingId.value}`, { |
| | | uuid: form.uuid, |
| | | word, |
| | | ...payload, |
| | | }); |
| | | ElMessage.success("已保存"); |
| | | } |
| | |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="职位"> |
| | | <el-select v-model="filters.position" placeholder="请选择" clearable> |
| | | <el-option label="部门负责人" value="LEADER" /> |
| | | <el-option label="员工" value="STAFF" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="状态"> |
| | | <el-select v-model="filters.status" placeholder="请选择" clearable> |
| | | <el-option label="启用" :value="1" /> |
| | |
| | | <el-table-column prop="staffNo" label="员工编号" width="120" show-overflow-tooltip /> |
| | | <el-table-column prop="mobileMasked" label="联系方式" width="130" /> |
| | | <el-table-column prop="deptName" label="所属部门" min-width="120" show-overflow-tooltip /> |
| | | <el-table-column prop="positionLabel" label="所属职位" width="120" /> |
| | | <el-table-column prop="groupDisplay" label="所属分组" min-width="170" show-overflow-tooltip /> |
| | | <el-table-column label="状态" width="92"> |
| | | <template #default="{ row }"> |
| | |
| | | class="fullw" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="职位" required> |
| | | <el-radio-group v-model="form.deptLeader"> |
| | | <el-radio :label="false">员工</el-radio> |
| | | <el-radio :label="true">部门负责人</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="detailGroupNames.length" label="所属分组"> |
| | | <div class="group-tags"> |
| | | <el-tag v-for="g in detailGroupNames" :key="g" size="small" class="g-tag">{{ g }}</el-tag> |
| | |
| | | import { Refresh, Search } from "@element-plus/icons-vue"; |
| | | import type { UploadRequestOptions } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type DeptTree = { id: number; label: string; children?: DeptTree[] }; |
| | | type StaffRow = { |
| | |
| | | realName: "", |
| | | staffNo: "", |
| | | mobile: "", |
| | | position: "" as string, |
| | | status: undefined as number | undefined, |
| | | staffGroupId: undefined as number | undefined, |
| | | }); |
| | |
| | | realName: filters.realName?.trim() || undefined, |
| | | staffNo: filters.staffNo?.trim() || undefined, |
| | | mobile: filters.mobile?.trim() || undefined, |
| | | position: filters.position || undefined, |
| | | status: filters.status, |
| | | staffGroupId: filters.staffGroupId, |
| | | }; |
| | |
| | | filters.realName = ""; |
| | | filters.staffNo = ""; |
| | | filters.mobile = ""; |
| | | filters.position = ""; |
| | | filters.status = undefined; |
| | | filters.staffGroupId = undefined; |
| | | page.value = 1; |
| | |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | const body = { |
| | | const body = normalizeFormData({ |
| | | nickName: form.nickName.trim(), |
| | | staffNo: form.staffNo.trim() || undefined, |
| | | avatarUrl: form.avatarUrl.trim() || undefined, |
| | | mobile: form.mobile.trim(), |
| | | deptId: form.deptId, |
| | | deptLeader: form.deptLeader, |
| | | }; |
| | | deptLeader: false, |
| | | }); |
| | | if (editingId.value == null) { |
| | | await http.post("/sys/staff", body); |
| | | ElMessage.success("已添加"); |
| | |
| | | |
| | | .dept-panel { |
| | | padding: 14px 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .content-panel { |
| | |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | margin-bottom: 14px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .dept-search { |
| | | margin-bottom: 14px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .dept-tree-wrap { |
| | | min-height: 520px; |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .dept-all { |
| | |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { useRoute } from "vue-router"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type DeptTree = { id: number; label: string; children?: DeptTree[] }; |
| | | type StaffRow = { id: number; uuid: string; realName?: string; staffNo?: string; mobileMasked?: string; deptId?: number; deptName?: string }; |
| | |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await http.post(`/sys/staff-groups/${id}/members`, { uuid, memberIds }); |
| | | const payload = normalizeFormData({ uuid, memberIds }); |
| | | await http.post(`/sys/staff-groups/${id}/members`, payload); |
| | | ElMessage.success("已追加成员"); |
| | | picker.value = false; |
| | | pickedMap.value = new Map(); |
| | |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { useRouter } from "vue-router"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type GroupRow = { |
| | | id: number; |
| | |
| | | saving.value = true; |
| | | try { |
| | | if (!editing.value) { |
| | | await http.post("/sys/staff-groups", { |
| | | const payload = normalizeFormData({ |
| | | groupName: form.groupName.trim(), |
| | | leaderMpUserId: form.leaderMpUserId, |
| | | memberIds, |
| | | }); |
| | | await http.post("/sys/staff-groups", payload); |
| | | ElMessage.success("已创建"); |
| | | } else { |
| | | await http.put(`/sys/staff-groups/${editing.value.id}`, { |
| | | const payload = normalizeFormData({ |
| | | uuid: form.uuid, |
| | | groupName: form.groupName.trim(), |
| | | leaderMpUserId: form.leaderMpUserId, |
| | | memberIds, |
| | | }); |
| | | await http.put(`/sys/staff-groups/${editing.value.id}`, payload); |
| | | ElMessage.success("已保存"); |
| | | } |
| | | dlg.value = false; |
| | |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import type { UploadRequestOptions } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type RoleRow = { id: number; roleCode: string; roleName: string }; |
| | | |
| | |
| | | const nextStatus = isAdminUsername(createForm.username) ? 1 : (createForm.statusEnabled ? 1 : 0); |
| | | saving.value = true; |
| | | try { |
| | | await http.post("/sys/users", { |
| | | const payload = normalizeFormData({ |
| | | username: createForm.username.trim(), |
| | | password: createForm.password, |
| | | realName: createForm.realName || undefined, |
| | |
| | | roleIds: createForm.roleIds, |
| | | status: nextStatus, |
| | | }); |
| | | await http.post("/sys/users", payload); |
| | | ElMessage.success("已创建"); |
| | | dlgCreate.value = false; |
| | | load(); |
| | |
| | | const nextStatus = isAdminUsername(editForm.username) ? 1 : (editForm.statusEnabled ? 1 : 0); |
| | | saving.value = true; |
| | | try { |
| | | await http.put(`/sys/users/${editingId.value}`, { |
| | | const payload = normalizeFormData({ |
| | | uuid: editForm.uuid, |
| | | realName: editForm.realName || undefined, |
| | | mobile: editForm.mobile || undefined, |
| | |
| | | roleIds: editForm.roleIds, |
| | | status: nextStatus, |
| | | }); |
| | | await http.put(`/sys/users/${editingId.value}`, payload); |
| | | ElMessage.success("已保存"); |
| | | dlgEdit.value = false; |
| | | load(); |
| | |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await http.post(`/sys/users/${pwdUserId.value}/password`, { |
| | | const payload = normalizeFormData({ |
| | | uuid: pwdUserUuid.value, |
| | | newPassword: pwdForm.newPassword, |
| | | newPasswordConfirm: pwdForm.newPasswordConfirm, |
| | | }); |
| | | await http.post(`/sys/users/${pwdUserId.value}/password`, payload); |
| | | ElMessage.success("密码已重置"); |
| | | dlgPwd.value = false; |
| | | } catch (e: unknown) { |
| | |
| | | import { onMounted, reactive, ref } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | |
| | | type Row = { |
| | | id: number; |
| | |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | const payload = { |
| | | reasonName, |
| | | // 处理表单数据:字符串 null/undefined 转为空字符串 |
| | | const normalized = normalizeFormData({ |
| | | reasonName: form.reasonName, |
| | | sortOrder: Number(form.sortOrder) || 0, |
| | | }; |
| | | }); |
| | | |
| | | if (editingId.value == null) { |
| | | await http.post("/config/visit-reasons/manage", payload); |
| | | await http.post("/config/visit-reasons/manage", normalized); |
| | | ElMessage.success("已新增"); |
| | | page.value = 1; |
| | | } else { |
| | | await http.put(`/config/visit-reasons/manage/${editingId.value}`, { |
| | | ...payload, |
| | | uuid: form.uuid, |
| | | ...normalized, |
| | | }); |
| | | ElMessage.success("已保存"); |
| | | } |
| | |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { Refresh, Search } from "@element-plus/icons-vue"; |
| | | import http from "../api/http"; |
| | | import { normalizeFormData } from "../utils/formHelper"; |
| | | import { useAuthStore } from "../stores/auth"; |
| | | |
| | | type VisitorRow = { |
| | |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | await http.put(`/sys/visitor-users/${editingId.value}`, { |
| | | const payload = normalizeFormData({ |
| | | uuid: form.uuid, |
| | | userName: form.userName.trim(), |
| | | mobile: form.mobile.trim(), |
| | | gender: form.gender || undefined, |
| | | companyName: form.companyName.trim() || undefined, |
| | | }); |
| | | await http.put(`/sys/visitor-users/${editingId.value}`, payload); |
| | | const nextStatus = form.statusEnabled ? 1 : 0; |
| | | if (nextStatus !== originalStatus.value) { |
| | | await http.patch(`/sys/visitor-users/${editingId.value}/status`, { |
| | | const statusPayload = normalizeFormData({ |
| | | uuid: form.uuid, |
| | | status: nextStatus, |
| | | }); |
| | | await http.patch(`/sys/visitor-users/${editingId.value}/status`, statusPayload); |
| | | } |
| | | ElMessage.success("已保存"); |
| | | dlg.value = false; |
| | |
| | | return '' |
| | | } |
| | | if (profile.value.userType === 'EMPLOYEE') { |
| | | return profile.value.deptLeaderFlag ? '部门领导' : '员工' |
| | | return '员工' |
| | | } |
| | | return '' |
| | | }) |
| | |
| | | const userRole = computed<'employee' | 'normal'>(() => profile.value?.userType === 'EMPLOYEE' ? 'employee' : 'normal') |
| | | const roleLabel = computed(() => { |
| | | if (userRole.value === 'employee') { |
| | | return profile.value?.deptLeaderFlag ? '部门领导' : '员工' |
| | | return '员工' |
| | | } |
| | | return '普通访客' |
| | | }) |
| | |
| | | userType: data.userType, |
| | | deptId: data.deptId ?? null, |
| | | deptName: data.deptName ?? null, |
| | | deptLeaderFlag: Boolean(data.deptLeader), |
| | | deptLeaderFlag: false, |
| | | avatarUrl: data.avatarUrl ?? null, |
| | | displayName: normalizeProfileDisplayName(data.displayName), |
| | | mobile: null, |
| | |
| | | userType: data.userType, |
| | | deptId: data.deptId ?? null, |
| | | deptName: data.deptName ?? null, |
| | | deptLeaderFlag: Boolean(data.deptLeaderFlag), |
| | | deptLeaderFlag: false, |
| | | avatarUrl: data.avatarUrl ?? null, |
| | | displayName: normalizeProfileDisplayName(data.nickName), |
| | | mobile: data.mobile ?? null, |
| | |
| | | } |
| | | } |
| | | |
| | | function sortVisitReasons(reasons: MiniVisitReasonItem[]) { |
| | | return [...reasons].sort((left, right) => { |
| | | const leftIsOther = left.reasonCode === 'OTHER' || left.reasonName === '其他' |
| | | const rightIsOther = right.reasonCode === 'OTHER' || right.reasonName === '其他' |
| | | if (leftIsOther !== rightIsOther) { |
| | | return leftIsOther ? 1 : -1 |
| | | } |
| | | |
| | | const leftOrder = left.sortOrder ?? Number.MAX_SAFE_INTEGER |
| | | const rightOrder = right.sortOrder ?? Number.MAX_SAFE_INTEGER |
| | | if (leftOrder !== rightOrder) { |
| | | return leftOrder - rightOrder |
| | | } |
| | | return left.id - right.id |
| | | }) |
| | | } |
| | | |
| | | async function loadVisitReasons() { |
| | | try { |
| | | visitReasons.value = await fetchVisitReasons() |
| | | visitReasons.value = sortVisitReasons(await fetchVisitReasons()) |
| | | if (selectedVisitReason.value) { |
| | | const nextReason = visitReasons.value.find(item => item.reasonCode === selectedVisitReason.value?.reasonCode) || null |
| | | selectedVisitReason.value = nextReason |
| | |
| | | return '' |
| | | } |
| | | if (profile.value.userType === 'EMPLOYEE') { |
| | | return profile.value.deptLeaderFlag ? '部门领导' : '员工' |
| | | return '员工' |
| | | } |
| | | return '' |
| | | }) |
| | |
| | | const userRole = computed<'employee' | 'normal'>(() => profile.value?.userType === 'EMPLOYEE' ? 'employee' : 'normal') |
| | | const roleLabel = computed(() => { |
| | | if (userRole.value === 'employee') { |
| | | return profile.value?.deptLeaderFlag ? '部门领导' : '员工' |
| | | return '员工' |
| | | } |
| | | return '普通访客' |
| | | }) |
| | |
| | | const selectedAreaIds = ref<number[]>([]) |
| | | const selectedAreaParentId = ref<number | null>(null) |
| | | const selectedAreaChildId = ref<number | null>(null) |
| | | const regionColumns = ref<string[][]>([ |
| | | ['东区', '西区', 'AA区'], |
| | | ['A部', 'B部', 'C部'], |
| | | ]) |
| | | const regionPickerIndex = ref([1, 1]) |
| | | const regionValue = ref(['西区', 'B部']) |
| | | const activeAreaParentId = ref<number | null>(null) |
| | | |
| | | const unitInput = ref('') |
| | | const unitList = ref<string[]>([]) |
| | |
| | | } |
| | | |
| | | function buildRegionColumns(parentIndex: number, childIndex: number) { |
| | | // 树形展开模式不需要此方法 |
| | | } |
| | | |
| | | function isAreaSelected(areaId: number) { |
| | | return selectedAreaIds.value.includes(areaId) |
| | | } |
| | | |
| | | const activeAreaParent = computed(() => { |
| | | if (areaTree.value.length === 0) { |
| | | return null |
| | | } |
| | | return areaTree.value.find(item => item.id === activeAreaParentId.value) || areaTree.value[0] |
| | | }) |
| | | |
| | | const activeAreaChildren = computed(() => activeAreaParent.value?.children || []) |
| | | |
| | | function getAreaSelectableIds(parent: MiniPassAccessAreaTreeItem) { |
| | | const childIds = (parent.children || []).map(child => child.id) |
| | | return childIds.length > 0 ? childIds : [parent.id] |
| | | } |
| | | |
| | | function isParentAreaSelected(parent: MiniPassAccessAreaTreeItem) { |
| | | const ids = getAreaSelectableIds(parent) |
| | | return ids.length > 0 && ids.every(id => selectedAreaIds.value.includes(id)) |
| | | } |
| | | |
| | | function isParentAreaPartialSelected(parent: MiniPassAccessAreaTreeItem) { |
| | | const ids = getAreaSelectableIds(parent) |
| | | return ids.some(id => selectedAreaIds.value.includes(id)) && !ids.every(id => selectedAreaIds.value.includes(id)) |
| | | } |
| | | |
| | | function selectActiveParent(parentId: number) { |
| | | activeAreaParentId.value = parentId |
| | | } |
| | | |
| | | function toggleParentAreaSelection(parent: MiniPassAccessAreaTreeItem) { |
| | | if (isHandlerReadOnly()) { |
| | | return |
| | | } |
| | | const safeParentIndex = Math.min(Math.max(parentIndex, 0), areaTree.value.length - 1) |
| | | const parent = areaTree.value[safeParentIndex] |
| | | const childOptions = parent.children && parent.children.length > 0 |
| | | ? parent.children |
| | | : [{ id: parent.id, areaName: parent.areaName, children: [] }] |
| | | const safeChildIndex = Math.min(Math.max(childIndex, 0), childOptions.length - 1) |
| | | regionColumns.value = [ |
| | | areaTree.value.map(item => item.areaName), |
| | | childOptions.map(item => item.areaName), |
| | | ] |
| | | regionPickerIndex.value = [safeParentIndex, safeChildIndex] |
| | | regionValue.value = [ |
| | | regionColumns.value[0][safeParentIndex] || '', |
| | | regionColumns.value[1][safeChildIndex] || '', |
| | | ] |
| | | activeAreaParentId.value = parent.id |
| | | const ids = getAreaSelectableIds(parent) |
| | | if (isParentAreaSelected(parent)) { |
| | | const removeIds = new Set(ids) |
| | | selectedAreaIds.value = selectedAreaIds.value.filter(id => !removeIds.has(id)) |
| | | } |
| | | else { |
| | | selectedAreaIds.value = Array.from(new Set([...selectedAreaIds.value, ...ids])) |
| | | } |
| | | syncSelectedAreaDisplay() |
| | | } |
| | | |
| | | function toggleChildAreaSelection(child: MiniPassAccessAreaTreeItem) { |
| | | if (isHandlerReadOnly()) { |
| | | return |
| | | } |
| | | if (selectedAreaIds.value.includes(child.id)) { |
| | | selectedAreaIds.value = selectedAreaIds.value.filter(id => id !== child.id) |
| | | } |
| | | else { |
| | | selectedAreaIds.value = [...selectedAreaIds.value, child.id] |
| | | } |
| | | syncSelectedAreaDisplay() |
| | | } |
| | | |
| | | function resolveAreaDisplayById(targetId: number) { |
| | |
| | | if (areaTree.value.length === 0) { |
| | | return |
| | | } |
| | | let parentIndex = 0 |
| | | let childIndex = 0 |
| | | const targetId = selectedAreaIds.value[selectedAreaIds.value.length - 1] |
| | | if (targetId) { |
| | | areaTree.value.some((parent, pIndex) => { |
| | | if (parent.id === targetId) { |
| | | parentIndex = pIndex |
| | | childIndex = 0 |
| | | selectedAreaParentId.value = parent.id |
| | | selectedAreaChildId.value = null |
| | | return true |
| | | // 自动展开已选中区域的父节点 |
| | | areaTree.value.forEach((parent) => { |
| | | const hasSelectedChild = (parent.children || []).some(child => selectedAreaIds.value.includes(child.id)) |
| | | if (hasSelectedChild || selectedAreaIds.value.includes(parent.id)) { |
| | | if (activeAreaParentId.value == null) { |
| | | activeAreaParentId.value = parent.id |
| | | } |
| | | const cIndex = (parent.children || []).findIndex(child => child.id === targetId) |
| | | if (cIndex >= 0) { |
| | | parentIndex = pIndex |
| | | childIndex = cIndex |
| | | selectedAreaParentId.value = parent.id |
| | | selectedAreaChildId.value = parent.children[cIndex].id |
| | | return true |
| | | } |
| | | return false |
| | | }) |
| | | } |
| | | }) |
| | | if (activeAreaParentId.value == null && areaTree.value.length > 0) { |
| | | activeAreaParentId.value = areaTree.value[0].id |
| | | } |
| | | buildRegionColumns(parentIndex, childIndex) |
| | | syncSelectedAreaDisplay() |
| | | } |
| | | |
| | |
| | | try { |
| | | areaTree.value = await fetchPassAccessAreaTree() |
| | | if (areaTree.value.length > 0) { |
| | | if (activeAreaParentId.value == null || !areaTree.value.some(parent => parent.id === activeAreaParentId.value)) { |
| | | activeAreaParentId.value = areaTree.value[0].id |
| | | } |
| | | syncRegionFromSelectedIds() |
| | | } |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ title: error instanceof Error ? error.message : '加载通行区域失败', icon: 'none' }) |
| | | } |
| | | } |
| | | |
| | | function onRegionConfirm(e: any) { |
| | | if (isHandlerReadOnly()) { |
| | | return |
| | | } |
| | | const value = e.detail?.value || [] |
| | | if (value.length < 2 || areaTree.value.length === 0) { |
| | | return |
| | | } |
| | | const parentChanged = Number(value[0]) !== regionPickerIndex.value[0] |
| | | buildRegionColumns(Number(value[0]), parentChanged ? 0 : Number(value[1])) |
| | | } |
| | | |
| | | function resolveCurrentRegionSelection() { |
| | | if (areaTree.value.length === 0) { |
| | | return null |
| | | } |
| | | const parent = areaTree.value[regionPickerIndex.value[0]] |
| | | if (!parent) { |
| | | return null |
| | | } |
| | | const childOptions = parent.children && parent.children.length > 0 |
| | | ? parent.children |
| | | : [{ id: parent.id, areaName: parent.areaName, children: [] }] |
| | | const child = childOptions[regionPickerIndex.value[1]] |
| | | const pickedId = child && child.id !== parent.id ? child.id : parent.id |
| | | const selectedNames = child && child.areaName !== parent.areaName |
| | | ? [parent.areaName, child.areaName] |
| | | : [parent.areaName] |
| | | return { |
| | | parentId: parent.id, |
| | | childId: child?.id ?? null, |
| | | pickedId, |
| | | value: [selectedNames[0], selectedNames[selectedNames.length - 1]], |
| | | } |
| | | } |
| | | |
| | | const currentRegionPickedId = computed(() => resolveCurrentRegionSelection()?.pickedId ?? null) |
| | | const isCurrentRegionSelected = computed(() => { |
| | | const pickedId = currentRegionPickedId.value |
| | | return pickedId != null && selectedAreaIds.value.includes(pickedId) |
| | | }) |
| | | |
| | | function toggleCurrentRegionSelection() { |
| | | if (isHandlerReadOnly()) { |
| | | return |
| | | } |
| | | const current = resolveCurrentRegionSelection() |
| | | if (!current) { |
| | | return |
| | | } |
| | | selectedAreaParentId.value = current.parentId |
| | | selectedAreaChildId.value = current.childId |
| | | regionValue.value = current.value |
| | | if (selectedAreaIds.value.includes(current.pickedId)) { |
| | | selectedAreaIds.value = selectedAreaIds.value.filter(item => item !== current.pickedId) |
| | | } |
| | | else { |
| | | selectedAreaIds.value = [...selectedAreaIds.value, current.pickedId] |
| | | } |
| | | syncSelectedAreaDisplay() |
| | | } |
| | | |
| | | function confirmRegionPicker() { |
| | |
| | | |
| | | <!-- 通行区域选择器 --> |
| | | <wd-popup v-model="showRegionPicker" position="bottom" custom-style="border-radius: 24rpx 24rpx 0 0; z-index: 9999;"> |
| | | <view class="w-full bg-white rounded-t-[24rpx] flex flex-col h-[860rpx]"> |
| | | <view class="w-full bg-white rounded-t-[24rpx] flex flex-col h-[900rpx]"> |
| | | <!-- 头部:关闭图标和标题 --> |
| | | <view class="flex items-center justify-between h-[108rpx] px-[32rpx] border-b border-solid border-[#F0F0F0] shrink-0"> |
| | | <view class="w-[48rpx] h-[48rpx] flex items-center justify-center" @click="showRegionPicker = false"> |
| | | <image src="/static/review/close@2x.png" class="w-[48rpx] h-[48rpx]" mode="aspectFit" /> |
| | | </view> |
| | | <text class="text-[#333333] text-[32rpx] font-medium flex-1 text-center pr-[48rpx]">选择通行区域</text> |
| | | </view> |
| | | |
| | | <!-- 选择器区域 --> |
| | | <view class="flex flex-col justify-center px-[32rpx] h-[330rpx] relative shrink-0"> |
| | | <view class="absolute left-[32rpx] right-[32rpx] top-1/2 -translate-y-1/2 h-[96rpx] bg-[#F4F8FF] rounded-[8rpx] z-0"></view> |
| | | <picker-view |
| | | class="custom-picker-view region-picker-view relative z-10" |
| | | :value="regionPickerIndex" |
| | | indicator-style="height: 48px; background-color: transparent;" |
| | | style="width: 100%; height: 192px;" |
| | | @change="onRegionConfirm" |
| | | > |
| | | <picker-view-column v-for="(col, colIndex) in regionColumns" :key="colIndex"> |
| | | <view |
| | | v-for="(item, itemIndex) in col" |
| | | :key="itemIndex" |
| | | class="flex items-center justify-center h-[48px] wd-picker-view-column__item" |
| | | :class="{ 'wd-picker-view-column__item--active': regionPickerIndex[colIndex] === itemIndex }" |
| | | > |
| | | {{ item }} |
| | | </view> |
| | | </picker-view-column> |
| | | </picker-view> |
| | | </view> |
| | | |
| | | <view class="px-[32rpx] pb-[24rpx] shrink-0"> |
| | | <view |
| | | class="w-full h-[72rpx] rounded-[36rpx] flex items-center justify-center border-[2rpx] border-solid active:opacity-80" |
| | | :class="isCurrentRegionSelected ? 'bg-[#F0F6FF] border-[#0068FF]' : 'bg-white border-[#0068FF]'" |
| | | @click="toggleCurrentRegionSelection" |
| | | > |
| | | <text class="text-[28rpx]" :class="isCurrentRegionSelected ? 'text-[#0068FF]' : 'text-[#0068FF]'"> |
| | | {{ isCurrentRegionSelected ? '已选择,点击取消' : '添加当前区域' }} |
| | | </text> |
| | | <view class="relative h-[108rpx] px-[32rpx] border-b border-solid border-[#F0F0F0] shrink-0 flex items-center justify-center"> |
| | | <text class="text-[#333333] text-[32rpx] font-medium">选择访问区域</text> |
| | | <view class="absolute right-[32rpx] top-[30rpx] w-[48rpx] h-[48rpx] flex items-center justify-center rounded-full bg-[#F5F7FB]" @click="showRegionPicker = false"> |
| | | <text class="text-[#9AA4B2] text-[32rpx] leading-none">×</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="px-[32rpx] pb-[16rpx] shrink-0"> |
| | | <view class="flex items-center justify-between mb-[16rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-medium">已选区域</text> |
| | | <text class="text-[#999999] text-[24rpx]">共{{ selectedAreaIds.length }}个</text> |
| | | </view> |
| | | <scroll-view scroll-y class="h-[150rpx]"> |
| | | <view v-if="selectedAreaIds.length > 0" class="flex flex-wrap gap-[14rpx] pb-[16rpx]"> |
| | | <view v-for="areaId in selectedAreaIds" :key="areaId" class="flex items-center justify-center px-[24rpx] h-[56rpx] rounded-[28rpx] border-[2rpx] border-solid border-[#E5E5E5] box-border"> |
| | | <text class="text-[#666666] text-[24rpx]">{{ resolveAreaDisplayById(areaId) }}</text> |
| | | <view class="flex items-center justify-center w-[24rpx] h-[24rpx] ml-[8rpx] active:opacity-80" @click.stop="removeSelectedArea(areaId)"> |
| | | <image src="/static/review/close.png" class="w-[24rpx] h-[24rpx]" mode="aspectFit" /> |
| | | <!-- 左右两栏选择区域 --> |
| | | <view class="relative flex-1 mx-[24rpx] mt-[24rpx] rounded-[24rpx] border border-solid border-[#EDF1F7] overflow-hidden flex min-h-0"> |
| | | <scroll-view scroll-y class="w-[350rpx] bg-white py-[12rpx] px-[12rpx] box-border"> |
| | | <view |
| | | v-for="parent in areaTree" |
| | | :key="parent.id" |
| | | class="h-[78rpx] mb-[10rpx] rounded-[12rpx] px-[14rpx] flex items-center justify-between" |
| | | :class="activeAreaParent?.id === parent.id ? 'bg-[#EEF4FF]' : 'bg-white'" |
| | | @click="selectActiveParent(parent.id)" |
| | | > |
| | | <view class="flex items-center min-w-0 flex-1" @click.stop="toggleParentAreaSelection(parent)"> |
| | | <view |
| | | class="w-[34rpx] h-[34rpx] rounded-[8rpx] border-[2rpx] border-solid flex items-center justify-center mr-[14rpx] shrink-0" |
| | | :class="isParentAreaSelected(parent) || isParentAreaPartialSelected(parent) ? 'bg-[#2F74FF] border-[#2F74FF]' : 'bg-white border-[#E2E8F0]'" |
| | | > |
| | | <view v-if="isParentAreaSelected(parent)" class="checkbox-icon"></view> |
| | | <view v-else-if="isParentAreaPartialSelected(parent)" class="w-[16rpx] h-[4rpx] bg-white rounded-full"></view> |
| | | </view> |
| | | <text class="text-[26rpx] font-medium truncate" :class="isParentAreaSelected(parent) || isParentAreaPartialSelected(parent) ? 'text-[#2F74FF]' : 'text-[#4A5568]'">{{ parent.areaName }}</text> |
| | | </view> |
| | | </view> |
| | | <view v-else class="h-[96rpx] flex items-center justify-center"> |
| | | <text class="text-[#B3B3B3] text-[24rpx]">请选择至少一个通行区域</text> |
| | | <text class="text-[30rpx]" :class="activeAreaParent?.id === parent.id ? 'text-[#2F74FF]' : 'text-[#B8C0CC]'">›</text> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <scroll-view scroll-y class="flex-1 bg-white py-[12rpx] px-[12rpx] box-border border-l border-solid border-[#F2F5FA]"> |
| | | <view |
| | | v-for="child in activeAreaChildren" |
| | | :key="child.id" |
| | | class="h-[78rpx] mb-[12rpx] rounded-[12rpx] px-[14rpx] flex items-center bg-[#EEF4FF]" |
| | | @click="toggleChildAreaSelection(child)" |
| | | > |
| | | <view |
| | | class="w-[34rpx] h-[34rpx] rounded-[8rpx] border-[2rpx] border-solid flex items-center justify-center mr-[14rpx] shrink-0" |
| | | :class="isAreaSelected(child.id) ? 'bg-[#2F74FF] border-[#2F74FF]' : 'bg-white border-[#E2E8F0]'" |
| | | > |
| | | <view v-if="isAreaSelected(child.id)" class="checkbox-icon"></view> |
| | | </view> |
| | | <text class="text-[26rpx] font-medium truncate" :class="isAreaSelected(child.id) ? 'text-[#2F74FF]' : 'text-[#4A5568]'">{{ child.areaName }}</text> |
| | | </view> |
| | | |
| | | <view v-if="activeAreaParent && activeAreaChildren.length === 0" class="h-[180rpx] flex items-center justify-center"> |
| | | <text class="text-[#B3B3B3] text-[26rpx]">当前区域无子区域</text> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view v-if="areaTree.length === 0" class="absolute inset-0 bg-white flex items-center justify-center"> |
| | | <text class="text-[#B3B3B3] text-[28rpx]">暂无通行区域</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 已选区域统计 --> |
| | | <view class="px-[32rpx] py-[16rpx] border-t border-solid border-[#F0F0F0] shrink-0"> |
| | | <view class="flex items-center justify-between"> |
| | | <text class="text-[#333333] text-[28rpx]">已选择</text> |
| | | <text class="text-[#0068FF] text-[28rpx] font-medium">{{ selectedAreaIds.length }} 个区域</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 底部确认按钮 --> |
| | |
| | | border-radius: 28rpx; |
| | | background: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 69%); |
| | | } |
| | | |
| | | /* 复选框勾选图标 - 使用CSS绘制 */ |
| | | .checkbox-icon { |
| | | width: 16rpx; |
| | | height: 10rpx; |
| | | border-left: 3rpx solid #FFFFFF; |
| | | border-bottom: 3rpx solid #FFFFFF; |
| | | transform: rotate(-45deg); |
| | | margin-top: -4rpx; |
| | | } |
| | | |
| | | /* 箭头图标 - 使用CSS绘制 */ |
| | | .arrow-icon { |
| | | width: 16rpx; |
| | | height: 16rpx; |
| | | border-top: 3rpx solid #666666; |
| | | border-right: 3rpx solid #666666; |
| | | transition: transform 0.3s ease; |
| | | } |
| | | |
| | | .arrow-down { |
| | | transform: rotate(135deg); |
| | | margin-top: -6rpx; |
| | | } |
| | | |
| | | .arrow-up { |
| | | transform: rotate(-45deg); |
| | | margin-bottom: -6rpx; |
| | | } |
| | | </style> |
| | | |
| | | <style> |
| | |
| | | userType: data.userType, |
| | | deptId: data.deptId ?? null, |
| | | deptName: data.deptName ?? null, |
| | | deptLeaderFlag: Boolean(data.deptLeader), |
| | | deptLeaderFlag: false, |
| | | avatarUrl: data.avatarUrl ?? null, |
| | | displayName: normalizeProfileDisplayName(data.displayName), |
| | | mobile: null, |
| | |
| | | userType: data.userType, |
| | | deptId: data.deptId ?? null, |
| | | deptName: data.deptName ?? null, |
| | | deptLeaderFlag: Boolean(data.deptLeaderFlag), |
| | | deptLeaderFlag: false, |
| | | avatarUrl: data.avatarUrl ?? null, |
| | | displayName: normalizeProfileDisplayName(data.nickName), |
| | | mobile: data.mobile ?? null, |