| | |
| | | import com.sinata.appointment.dto.FieldConfigPatchRequest; |
| | | import com.sinata.appointment.dto.SensitiveWordCreateRequest; |
| | | import com.sinata.appointment.dto.SensitiveWordEnabledPatchRequest; |
| | | import com.sinata.appointment.dto.SensitiveWordImportResultVo; |
| | | import com.sinata.appointment.dto.SensitiveWordListVo; |
| | | import com.sinata.appointment.dto.SensitiveWordUpdateRequest; |
| | | import com.sinata.appointment.dto.VisitReasonCreateRequest; |
| | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import java.util.List; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.http.HttpHeaders; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.web.bind.annotation.DeleteMapping; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PatchMapping; |
| | |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | @Tag(name = "系统配置") |
| | | @RestController |
| | |
| | | return ResultUtil.success(); |
| | | } |
| | | |
| | | @Operation(summary = "敏感词库-导入") |
| | | @PostMapping("/sensitive-words/manage/import") |
| | | @SaCheckPermission("config:sensitive-words:import") |
| | | public Result<SensitiveWordImportResultVo> importSensitiveWords(@RequestParam("file") MultipartFile file) { |
| | | return ResultUtil.data(configApplicationService.importSensitiveWords(file)); |
| | | } |
| | | |
| | | @Operation(summary = "敏感词库-导入模板") |
| | | @GetMapping("/sensitive-words/manage/import-template") |
| | | @SaCheckPermission("config:sensitive-words:import") |
| | | public ResponseEntity<byte[]> sensitiveWordImportTemplate() { |
| | | return ResponseEntity.ok() |
| | | .contentType( |
| | | MediaType.parseMediaType( |
| | | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) |
| | | .header( |
| | | HttpHeaders.CONTENT_DISPOSITION, |
| | | "attachment; filename=\"sensitive-word-import-template.xlsx\"") |
| | | .body(configApplicationService.sensitiveWordImportTemplateExcel()); |
| | | } |
| | | |
| | | @GetMapping("/fields") |
| | | @SaCheckPermission("config:booking-settings:page") |
| | | public Result<List<BizFieldConfig>> listFields() { |
| | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import java.util.List; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.web.bind.annotation.DeleteMapping; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PatchMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | |
| | | miniVisitorApplicationService.setDefault(id, uuid); |
| | | return ResultUtil.success(); |
| | | } |
| | | |
| | | @Operation(summary = "删除访客") |
| | | @DeleteMapping("/{id}") |
| | | public Result<Void> delete( |
| | | @PathVariable Long id, |
| | | @RequestParam @NotBlank(message = "uuid 不能为空") String uuid) { |
| | | miniVisitorApplicationService.delete(id, uuid); |
| | | return ResultUtil.success(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.sinata.appointment.dto; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | @Schema(description = "敏感词导入结果") |
| | | public class SensitiveWordImportResultVo { |
| | | |
| | | @Schema(description = "读取到的数据行数") |
| | | private int totalRows; |
| | | |
| | | @Schema(description = "成功导入数量") |
| | | private int successCount; |
| | | |
| | | @Schema(description = "重复跳过数量") |
| | | private int duplicateCount; |
| | | |
| | | @Schema(description = "无效跳过数量") |
| | | private int invalidCount; |
| | | |
| | | @Schema(description = "错误提示,最多返回前20条") |
| | | private List<String> errorMessages = new ArrayList<>(); |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.alibaba.excel.EasyExcel; |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.sinata.appointment.constants.BizErrorCode; |
| | |
| | | import com.sinata.appointment.dto.FieldConfigPatchRequest; |
| | | import com.sinata.appointment.dto.SensitiveWordCreateRequest; |
| | | import com.sinata.appointment.dto.SensitiveWordEnabledPatchRequest; |
| | | import com.sinata.appointment.dto.SensitiveWordImportResultVo; |
| | | import com.sinata.appointment.dto.SensitiveWordListVo; |
| | | import com.sinata.appointment.dto.SensitiveWordUpdateRequest; |
| | | import com.sinata.appointment.dto.VisitReasonCreateRequest; |
| | |
| | | import com.sinata.appointment.mapper.SysBizSettingMapper; |
| | | import com.sinata.appointment.mapper.SysDeptMapper; |
| | | import com.sinata.appointment.util.UuidAssert; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.IOException; |
| | | import java.time.LocalDate; |
| | | import java.time.YearMonth; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.ArrayList; |
| | | import java.util.Comparator; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.UUID; |
| | | import java.util.stream.Collectors; |
| | |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | @Service |
| | | @RequiredArgsConstructor |
| | |
| | | loadActiveSensitiveWord(id, uuid); |
| | | bizSensitiveWordMapper.deleteById(id); |
| | | sensitiveWordFilterService.evictCache(); |
| | | } |
| | | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public SensitiveWordImportResultVo importSensitiveWords(MultipartFile file) { |
| | | if (file == null || file.isEmpty()) { |
| | | throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "导入文件不能为空"); |
| | | } |
| | | String filename = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase(); |
| | | if (!filename.endsWith(".xlsx") && !filename.endsWith(".xls")) { |
| | | throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "请上传 xls 或 xlsx 文件"); |
| | | } |
| | | |
| | | List<Map<Integer, String>> rows; |
| | | try { |
| | | rows = EasyExcel.read(file.getInputStream()).sheet().headRowNumber(0).doReadSync(); |
| | | } catch (IOException ex) { |
| | | throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "读取导入文件失败"); |
| | | } |
| | | |
| | | SensitiveWordImportResultVo result = new SensitiveWordImportResultVo(); |
| | | Set<String> fileWords = new LinkedHashSet<>(); |
| | | Set<String> existingWords = loadExistingSensitiveWords(); |
| | | int rowNo = 0; |
| | | for (Map<Integer, String> row : rows) { |
| | | rowNo++; |
| | | String raw = row == null ? null : row.get(0); |
| | | if (rowNo == 1 && isSensitiveWordHeader(raw)) { |
| | | continue; |
| | | } |
| | | if (!StringUtils.hasText(raw)) { |
| | | continue; |
| | | } |
| | | result.setTotalRows(result.getTotalRows() + 1); |
| | | String word; |
| | | try { |
| | | word = normalizeSensitiveWord(raw); |
| | | } catch (BizException ex) { |
| | | result.setInvalidCount(result.getInvalidCount() + 1); |
| | | addImportError(result, "第" + rowNo + "行:" + ex.getMessage()); |
| | | continue; |
| | | } |
| | | if (fileWords.contains(word) || existingWords.contains(word)) { |
| | | result.setDuplicateCount(result.getDuplicateCount() + 1); |
| | | continue; |
| | | } |
| | | BizSensitiveWord entity = new BizSensitiveWord(); |
| | | entity.setWord(word); |
| | | entity.setEnabled(1); |
| | | bizSensitiveWordMapper.insert(entity); |
| | | fileWords.add(word); |
| | | existingWords.add(word); |
| | | result.setSuccessCount(result.getSuccessCount() + 1); |
| | | } |
| | | if (result.getSuccessCount() > 0) { |
| | | sensitiveWordFilterService.evictCache(); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | public byte[] sensitiveWordImportTemplateExcel() { |
| | | try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { |
| | | List<List<String>> head = List.of(List.of("敏感词")); |
| | | List<List<String>> rows = List.of(List.of("示例敏感词")); |
| | | EasyExcel.write(bos).head(head).sheet("敏感词导入").doWrite(rows); |
| | | return bos.toByteArray(); |
| | | } catch (IOException ex) { |
| | | throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "生成导入模板失败"); |
| | | } |
| | | } |
| | | |
| | | public List<BizFieldConfig> listFieldConfigs() { |
| | |
| | | return normalized; |
| | | } |
| | | |
| | | private Set<String> loadExistingSensitiveWords() { |
| | | List<BizSensitiveWord> rows = |
| | | bizSensitiveWordMapper.selectList( |
| | | new LambdaQueryWrapper<BizSensitiveWord>() |
| | | .eq(BizSensitiveWord::getDelFlag, 0) |
| | | .select(BizSensitiveWord::getWord)); |
| | | Set<String> words = new HashSet<>(); |
| | | for (BizSensitiveWord row : rows) { |
| | | if (StringUtils.hasText(row.getWord())) { |
| | | words.add(row.getWord().trim()); |
| | | } |
| | | } |
| | | return words; |
| | | } |
| | | |
| | | private boolean isSensitiveWordHeader(String value) { |
| | | if (!StringUtils.hasText(value)) { |
| | | return false; |
| | | } |
| | | String text = value.trim(); |
| | | return "敏感词".equals(text) || "敏感词库".equals(text) || "word".equalsIgnoreCase(text); |
| | | } |
| | | |
| | | private void addImportError(SensitiveWordImportResultVo result, String message) { |
| | | if (result.getErrorMessages().size() < 20) { |
| | | result.getErrorMessages().add(message); |
| | | } |
| | | } |
| | | |
| | | private void assertReasonNameUnique(String reasonName, Long excludeId) { |
| | | LambdaQueryWrapper<BizVisitReason> q = |
| | | new LambdaQueryWrapper<BizVisitReason>() |
| | |
| | | import com.sinata.appointment.constants.ApprovalNodeCode; |
| | | import com.sinata.appointment.constants.ApprovalTaskStatus; |
| | | import com.sinata.appointment.entity.ApprovalTask; |
| | | import com.sinata.appointment.entity.AppointmentVisitor; |
| | | import com.sinata.appointment.entity.AppointmentOrder; |
| | | import com.sinata.appointment.entity.BizVisitReason; |
| | | import com.sinata.appointment.entity.MpUser; |
| | | import com.sinata.appointment.integration.WechatMiniGateway; |
| | | import com.sinata.appointment.mapper.ApprovalTaskMapper; |
| | | import com.sinata.appointment.mapper.AppointmentVisitorMapper; |
| | | import com.sinata.appointment.mapper.BizVisitReasonMapper; |
| | | import com.sinata.appointment.mapper.MpUserMapper; |
| | | import java.time.LocalDate; |
| | |
| | | public class IntegrationWechatService { |
| | | |
| | | private static final String APPROVAL_RESULT_TEMPLATE_ID = |
| | | "4FQovCBNFM7rth42AWHTusHajO788eT9VpplSN4ZG2s"; |
| | | "rIJpfN51_q818RTpJAqTe5ssTwWCHnxDM5rpq64uuKM"; |
| | | private static final String NOTICE_RESULT_TEMPLATE_ID = |
| | | "GC1S3T2qHCbz8ow130u7elRczuqW6Z8q54mKRCYE-xo"; |
| | | "Wip0ODJgybZpeFpcvev_0GK1wQXEqZw-LHOjS-UyDLw"; |
| | | private static final String APPROVAL_RESULT_PAGE = "pages/record/index"; |
| | | private static final String APPROVAL_TODO_TEMPLATE_ID = |
| | | "BX49OBHvRcoYiCw0ZLqx04BiWQ9KaEQwuVth_Lh_M2A"; |
| | | "QlNwG99WXWztRDgtlLcE0fHxfY2VwaX8PRMrwGpEync"; |
| | | private static final String APPROVAL_TODO_PAGE = "pages/reviewList/index"; |
| | | private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | private static final DateTimeFormatter TEMPLATE_TIME_FORMATTER = |
| | |
| | | private final WechatMiniGateway wechatMiniGateway; |
| | | private final MpUserMapper mpUserMapper; |
| | | private final ApprovalTaskMapper approvalTaskMapper; |
| | | private final AppointmentVisitorMapper appointmentVisitorMapper; |
| | | private final BizVisitReasonMapper bizVisitReasonMapper; |
| | | |
| | | @Async |
| | |
| | | return; |
| | | } |
| | | Map<String, String> templateData = new LinkedHashMap<>(); |
| | | templateData.put("thing1", approved ? "审核通过" : "审核未通过"); |
| | | templateData.put("thing2", abbreviate(resolveSubmitterName(submitter), 20)); |
| | | templateData.put("time3", buildVisitTemplateTimeValue(order)); |
| | | templateData.put("thing4", abbreviate(buildVisitReasonText(order), 20)); |
| | | templateData.put("phrase4", approved ? "通过" : "未通过"); |
| | | templateData.put("time2", buildVisitTemplateTimeValue(order)); |
| | | templateData.put("thing5", approved ? "预约审核通过" : "预约审核未通过"); |
| | | wechatMiniGateway.sendSubscribeMessage( |
| | | submitter.getOpenId(), APPROVAL_RESULT_TEMPLATE_ID, APPROVAL_RESULT_PAGE, templateData); |
| | | log.info( |
| | |
| | | return; |
| | | } |
| | | Map<String, String> templateData = new LinkedHashMap<>(); |
| | | templateData.put("thing6", buildVisitorCountText(visitorCount)); |
| | | templateData.put("time18", buildVisitTemplateTimeValue(order)); |
| | | templateData.put("thing20", abbreviate(buildNoticeBusinessTypeText(order, approved), 20)); |
| | | MpUser submitter = loadSubmitter(order); |
| | | templateData.put("name1", abbreviate(resolveSubmitterName(submitter), 10)); |
| | | templateData.put("number36", buildVisitorCountNumber(visitorCount)); |
| | | templateData.put("date3", buildVisitTemplateTimeValue(order)); |
| | | int successCount = 0; |
| | | int skippedCount = 0; |
| | | int failedCount = 0; |
| | |
| | | continue; |
| | | } |
| | | Map<String, String> templateData = new LinkedHashMap<>(); |
| | | templateData.put("thing1", abbreviate(buildTodoTaskName(order), 20)); |
| | | templateData.put("thing3", abbreviate(String.join("、", entry.getValue()), 20)); |
| | | MpUser submitter = loadSubmitter(order); |
| | | templateData.put("name1", abbreviate(resolveSubmitterName(submitter), 10)); |
| | | templateData.put("number15", buildVisitorCountNumber(countVisitors(order))); |
| | | templateData.put("time16", buildVisitTemplateTimeValue(order)); |
| | | templateData.put("thing5", abbreviate(buildVisitReasonText(order), 20)); |
| | | try { |
| | | wechatMiniGateway.sendSubscribeMessage( |
| | | user.getOpenId(), APPROVAL_TODO_TEMPLATE_ID, APPROVAL_TODO_PAGE, templateData); |
| | |
| | | } |
| | | |
| | | private String resolveSubmitterName(MpUser submitter) { |
| | | if (submitter == null) { |
| | | return "预约人"; |
| | | } |
| | | if (StringUtils.hasText(submitter.getNickName())) { |
| | | return submitter.getNickName().trim(); |
| | | } |
| | |
| | | return submitter.getMobile().trim(); |
| | | } |
| | | return "预约人"; |
| | | } |
| | | |
| | | private MpUser loadSubmitter(AppointmentOrder order) { |
| | | if (order == null || order.getSubmitterId() == null) { |
| | | return null; |
| | | } |
| | | return mpUserMapper.selectOne( |
| | | new LambdaQueryWrapper<MpUser>() |
| | | .eq(MpUser::getDelFlag, 0) |
| | | .eq(MpUser::getId, order.getSubmitterId()) |
| | | .last("limit 1")); |
| | | } |
| | | |
| | | private String buildVisitTimeText(AppointmentOrder order) { |
| | |
| | | return reasonCode; |
| | | } |
| | | |
| | | private String buildVisitorCountText(int visitorCount) { |
| | | return Math.max(visitorCount, 0) + "人"; |
| | | private String buildVisitorCountNumber(int visitorCount) { |
| | | return String.valueOf(Math.max(visitorCount, 0)); |
| | | } |
| | | |
| | | private String buildNoticeBusinessTypeText(AppointmentOrder order, boolean approved) { |
| | | String reason = buildVisitReasonText(order); |
| | | if (!StringUtils.hasText(reason)) { |
| | | return approved ? "预约通过" : "预约驳回"; |
| | | private int countVisitors(AppointmentOrder order) { |
| | | if (order == null || order.getId() == null) { |
| | | return 0; |
| | | } |
| | | return reason; |
| | | Long count = |
| | | appointmentVisitorMapper.selectCount( |
| | | new LambdaQueryWrapper<AppointmentVisitor>() |
| | | .eq(AppointmentVisitor::getDelFlag, 0) |
| | | .eq(AppointmentVisitor::getOrderId, order.getId())); |
| | | return count == null ? 0 : count.intValue(); |
| | | } |
| | | |
| | | private String buildTodoTaskName(AppointmentOrder order) { |
| | |
| | | import java.nio.file.StandardCopyOption; |
| | | import java.util.Set; |
| | | import java.util.UUID; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | @Service |
| | | @Slf4j |
| | | public class LocalImageUploadService { |
| | | |
| | | private static final long IMAGE_MAX_SIZE = 10L * 1024 * 1024; |
| | |
| | | } catch (IOException ex) { |
| | | throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "图片上传失败"); |
| | | } |
| | | return buildPictureUrl(fileName); |
| | | String url = buildPictureUrl(fileName); |
| | | log.info("local image uploaded. path={} url={}", target, url); |
| | | return url; |
| | | } |
| | | |
| | | private String buildPictureUrl(String fileName) { |
| | |
| | | mpVisitorMapper.updateById(row); |
| | | } |
| | | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void delete(Long id, String uuid) { |
| | | MpUser current = miniAuthApplicationService.currentMpUser(); |
| | | MpVisitor row = loadOwnedVisitor(id, uuid, current.getId()); |
| | | boolean wasDefault = isDefault(row); |
| | | mpVisitorMapper.deleteById(row.getId()); |
| | | if (wasDefault) { |
| | | resetEarliestVisitorAsDefault(current.getId()); |
| | | } |
| | | } |
| | | |
| | | public MiniVisitorDetailVo detail(Long id, String uuid) { |
| | | MpUser current = miniAuthApplicationService.currentMpUser(); |
| | | MpVisitor row = loadOwnedVisitor(id, uuid, current.getId()); |
| | |
| | | > 0; |
| | | } |
| | | |
| | | private void resetEarliestVisitorAsDefault(Long mpUserId) { |
| | | MpVisitor next = |
| | | mpVisitorMapper.selectOne( |
| | | new LambdaQueryWrapper<MpVisitor>() |
| | | .eq(MpVisitor::getMpUserId, mpUserId) |
| | | .eq(MpVisitor::getDelFlag, 0) |
| | | .orderByAsc(MpVisitor::getCreateTime) |
| | | .orderByAsc(MpVisitor::getId) |
| | | .last("limit 1")); |
| | | if (next != null) { |
| | | next.setIsDefault(1); |
| | | mpVisitorMapper.updateById(next); |
| | | } |
| | | } |
| | | |
| | | private List<String> resolveMissingFieldCodes(MpVisitor row, Set<String> requiredCodes) { |
| | | List<String> missing = new ArrayList<>(); |
| | | for (String code : requiredCodes) { |
| | |
| | | # 微信小程序(真实登录时设 stub: false 并配置 app-id / app-secret) |
| | | wechat: |
| | | miniapp: |
| | | app-id: ${WECHAT_MINI_APPID:wxe91f1af7638aa5dd} |
| | | app-secret: ${WECHAT_MINI_SECRET:a787e1a462715604e0c9528b6d8960d1} |
| | | app-id: ${WECHAT_MINI_APPID:wxd710abda41dee55d} |
| | | app-secret: ${WECHAT_MINI_SECRET:191a7eccc4f6c8d027ce47bd0c25da8f} |
| | | stub: ${WECHAT_MINI_STUB:false} |
| | | official-account: |
| | | app-id: ${WECHAT_OFFICIAL_APPID:wxd312008b67bf7292} |
| | |
| | | hikvision: |
| | | qrcode: |
| | | encrypt-enabled: ${HIKVISION_QRCODE_ENCRYPT_ENABLED:false} |
| | | aes-key: ${HIKVISION_QRCODE_AES_KEY:} |
| | | aes-iv: ${HIKVISION_QRCODE_AES_IV:} |
| | | aes-key: ${HIKVISION_QRCODE_AES_KEY:SinataHikvisionQrcodeKey01} |
| | | aes-iv: ${HIKVISION_QRCODE_AES_IV:SinataHikvisionQrcodeIv001} |
| | | valid-minutes: ${HIKVISION_QRCODE_VALID_MINUTES:0} |
| | | |
| | | file-upload-path: /opt/web/uploads |
| | | picture-server-address: https://ytx.hongyu.com/resources/ |
| | | file-upload-path: /opt/web/uploads/ |
| | | picture-server-address: https://yts.hongyu.com/resources/ |
| New file |
| | |
| | | INSERT INTO sys_menu (uuid, version, pid, menu_type, menu_name, perm_code, sort_order, route_path, del_flag) |
| | | SELECT REPLACE(UUID(), '-', ''), 1, p.id, 3, '导入', 'config:sensitive-words:import', 50, NULL, 0 |
| | | FROM sys_menu p |
| | | WHERE p.route_path = '/system/sensitive-words' |
| | | AND p.del_flag = 0 |
| | | AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perm_code = 'config:sensitive-words:import' AND del_flag = 0); |
| | | |
| | | INSERT INTO sys_role_menu (uuid, version, role_id, menu_id, del_flag) |
| | | SELECT LOWER(MD5(CONCAT(r.id, ':', m.id, ':sensitive-word-import'))), 1, r.id, m.id, 0 |
| | | FROM sys_role r |
| | | JOIN sys_menu m ON m.del_flag = 0 |
| | | LEFT JOIN sys_role_menu rm ON rm.role_id = r.id AND rm.menu_id = m.id AND rm.del_flag = 0 |
| | | WHERE r.role_code = 'ADMIN' |
| | | AND r.del_flag = 0 |
| | | AND m.perm_code = 'config:sensitive-words:import' |
| | | AND rm.id IS NULL; |
| | |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.sinata.appointment.entity.AppointmentOrder; |
| | | import com.sinata.appointment.entity.BizVisitReason; |
| | | import com.sinata.appointment.entity.MpUser; |
| | | import com.sinata.appointment.integration.WechatMiniGateway; |
| | | import com.sinata.appointment.mapper.ApprovalTaskMapper; |
| | | import com.sinata.appointment.mapper.AppointmentVisitorMapper; |
| | | import com.sinata.appointment.mapper.BizVisitReasonMapper; |
| | | import com.sinata.appointment.mapper.MpUserMapper; |
| | | import java.time.LocalDate; |
| | |
| | | private ApprovalTaskMapper approvalTaskMapper; |
| | | |
| | | @Mock |
| | | private AppointmentVisitorMapper appointmentVisitorMapper; |
| | | |
| | | @Mock |
| | | private BizVisitReasonMapper bizVisitReasonMapper; |
| | | |
| | | private IntegrationWechatService integrationWechatService; |
| | |
| | | wechatMiniGateway, |
| | | mpUserMapper, |
| | | approvalTaskMapper, |
| | | appointmentVisitorMapper, |
| | | bizVisitReasonMapper); |
| | | } |
| | | |
| | |
| | | submitter.setOpenId("openid-123"); |
| | | submitter.setNickName("张三"); |
| | | |
| | | BizVisitReason reason = new BizVisitReason(); |
| | | reason.setReasonCode("BUSINESS"); |
| | | reason.setReasonName("商务洽谈"); |
| | | |
| | | when(mpUserMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(submitter); |
| | | when(bizVisitReasonMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(reason); |
| | | |
| | | integrationWechatService.notifyApprovalFinalResult(order, true); |
| | | |
| | |
| | | verify(wechatMiniGateway) |
| | | .sendSubscribeMessage( |
| | | eq("openid-123"), |
| | | eq("4FQovCBNFM7rth42AWHTusHajO788eT9VpplSN4ZG2s"), |
| | | eq("rIJpfN51_q818RTpJAqTe5ssTwWCHnxDM5rpq64uuKM"), |
| | | eq("pages/record/index"), |
| | | dataCaptor.capture()); |
| | | |
| | | Map<String, String> payload = dataCaptor.getValue(); |
| | | assertEquals("审核通过", payload.get("thing1")); |
| | | assertEquals("张三", payload.get("thing2")); |
| | | assertEquals("2026-04-24 09:00", payload.get("time3")); |
| | | assertEquals("商务洽谈", payload.get("thing4")); |
| | | assertEquals("通过", payload.get("phrase4")); |
| | | assertEquals("2026-04-24 09:00", payload.get("time2")); |
| | | assertEquals("预约审核通过", payload.get("thing5")); |
| | | } |
| | | |
| | | @Test |
| | |
| | | <div class="page"> |
| | | <div class="header"> |
| | | <h3 class="section-title">敏感词库</h3> |
| | | <el-button v-permission="'config:sensitive-words:create'" type="primary" @click="openCreate">新增</el-button> |
| | | <div class="header-actions"> |
| | | <el-button v-permission="'config:sensitive-words:import'" type="primary" plain @click="downloadTemplate"> |
| | | 下载模板 |
| | | </el-button> |
| | | <el-upload |
| | | v-permission="'config:sensitive-words:import'" |
| | | :show-file-list="false" |
| | | accept=".xls,.xlsx" |
| | | :http-request="importSensitiveWords" |
| | | > |
| | | <el-button type="primary" :loading="importing">导入</el-button> |
| | | </el-upload> |
| | | <el-button v-permission="'config:sensitive-words:create'" type="primary" @click="openCreate">新增</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-form class="filters" :inline="true" :model="filters" @submit.prevent> |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import type { UploadRequestOptions } from "element-plus"; |
| | | import http from "../api/http"; |
| | | |
| | | type Row = { |
| | |
| | | |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | const importing = ref(false); |
| | | const rows = ref<Row[]>([]); |
| | | const page = ref(1); |
| | | const size = ref(10); |
| | |
| | | } |
| | | } |
| | | |
| | | async function importSensitiveWords(opt: UploadRequestOptions) { |
| | | const file = opt.file as File; |
| | | if (!file) return; |
| | | if (!/\.(xls|xlsx)$/i.test(file.name)) { |
| | | ElMessage.warning("请上传 xls 或 xlsx 文件"); |
| | | return; |
| | | } |
| | | const fd = new FormData(); |
| | | fd.append("file", file); |
| | | importing.value = true; |
| | | try { |
| | | const { data } = await http.post("/config/sensitive-words/manage/import", fd, { |
| | | headers: { "Content-Type": "multipart/form-data" }, |
| | | }); |
| | | const result = data.result || {}; |
| | | const success = Number(result.successCount) || 0; |
| | | const duplicate = Number(result.duplicateCount) || 0; |
| | | const invalid = Number(result.invalidCount) || 0; |
| | | const errors = Array.isArray(result.errorMessages) ? result.errorMessages : []; |
| | | const message = `导入完成:成功${success}条,重复跳过${duplicate}条,无效${invalid}条`; |
| | | if (errors.length) { |
| | | ElMessageBox.alert(`${message}\n${errors.join("\n")}`, "导入结果", { |
| | | confirmButtonText: "知道了", |
| | | }); |
| | | } else { |
| | | ElMessage.success(message); |
| | | } |
| | | page.value = 1; |
| | | load(); |
| | | } catch (e: unknown) { |
| | | ElMessage.error(e instanceof Error ? e.message : "导入失败"); |
| | | } finally { |
| | | importing.value = false; |
| | | } |
| | | } |
| | | |
| | | async function downloadTemplate() { |
| | | try { |
| | | const res = await http.get("/config/sensitive-words/manage/import-template", { responseType: "blob" }); |
| | | const blob = new Blob([res.data], { |
| | | type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
| | | }); |
| | | const url = window.URL.createObjectURL(blob); |
| | | const a = document.createElement("a"); |
| | | a.href = url; |
| | | a.download = "sensitive-word-import-template.xlsx"; |
| | | a.click(); |
| | | window.URL.revokeObjectURL(url); |
| | | } catch (e: unknown) { |
| | | ElMessage.error(e instanceof Error ? e.message : "下载模板失败"); |
| | | } |
| | | } |
| | | |
| | | function toggleEnabled(row: Row) { |
| | | const nextEnabled = row.enabled === 1 ? 0 : 1; |
| | | const actionText = nextEnabled === 1 ? "启用" : "禁用"; |
| | |
| | | margin-bottom: 12px; |
| | | flex: none; |
| | | } |
| | | .header-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | |
| | | const response = await alovaInstance.Patch<ApiResult<T>>(url, data, config).send() |
| | | return unwrapApiResult(response, '请求失败') |
| | | } |
| | | |
| | | export async function deleteResult<T>( |
| | | url: string, |
| | | config?: Parameters<typeof alovaInstance.Delete>[2], |
| | | ): Promise<T> { |
| | | const response = await alovaInstance.Delete<ApiResult<T>>(url, undefined, config).send() |
| | | return unwrapApiResult(response, '请求失败') |
| | | } |
| | |
| | | import { getResult, patchResult, postResult, putResult } from './core/request' |
| | | import { deleteResult, getResult, patchResult, postResult, putResult } from './core/request' |
| | | import type { |
| | | MiniVisitorDetailItem, |
| | | MiniVisitorListItem, |
| | |
| | | params: { uuid }, |
| | | }) |
| | | } |
| | | |
| | | export function deleteVisitor(id: number, uuid: string) { |
| | | return deleteResult<void>(`/api/mini/visitors/${id}`, { |
| | | params: { uuid }, |
| | | }) |
| | | } |
| | |
| | | const defineComponent: typeof import('vue')['defineComponent'] |
| | | const defineStore: typeof import('pinia')['defineStore'] |
| | | const deleteMyAppointment: typeof import('./api/appointment')['deleteMyAppointment'] |
| | | const deleteVisitor: typeof import('./api/visitor')['deleteVisitor'] |
| | | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] |
| | | const effectScope: typeof import('vue')['effectScope'] |
| | | const extendRef: typeof import('@vueuse/core')['extendRef'] |
| | |
| | | readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']> |
| | | readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']> |
| | | readonly deleteMyAppointment: UnwrapRef<typeof import('./api/appointment')['deleteMyAppointment']> |
| | | readonly deleteVisitor: UnwrapRef<typeof import('./api/visitor')['deleteVisitor']> |
| | | readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']> |
| | | readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> |
| | | readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']> |
| | |
| | | <script setup lang="ts"> |
| | | import { fetchMiniFieldConfigs } from '@/api/lookup' |
| | | import { fetchMyVisitors, setDefaultVisitor } from '@/api/visitor' |
| | | import { deleteVisitor, fetchMyVisitors, setDefaultVisitor } from '@/api/visitor' |
| | | import { FIELD_CODES, formatVisitorFieldLabels } from '@/constants/fieldCodes' |
| | | import type { MiniFieldConfigItem } from '@/types/lookup' |
| | | import type { MiniVisitorListItem } from '@/types/visitor' |
| | |
| | | const visitors = ref<MiniVisitorListItem[]>([]) |
| | | const loading = ref(false) |
| | | const settingDefaultId = ref<number | null>(null) |
| | | const deletingId = ref<number | null>(null) |
| | | const openSwipeId = ref<number | null>(null) |
| | | const fieldConfigs = ref<MiniFieldConfigItem[]>([]) |
| | | |
| | | let touchStartX = 0 |
| | | let touchStartY = 0 |
| | | let touchingId: number | null = null |
| | | |
| | | const showName = computed(() => isVisible(FIELD_CODES.VISITOR_NAME)) |
| | | const showGender = computed(() => isVisible(FIELD_CODES.GENDER)) |
| | |
| | | } |
| | | |
| | | function handleEdit(item: MiniVisitorListItem) { |
| | | openSwipeId.value = null |
| | | uni.navigateTo({ |
| | | url: `/pages/addPerson/index?id=${item.id}&uuid=${item.uuid}`, |
| | | }) |
| | |
| | | if (item.isDefault || settingDefaultId.value) { |
| | | return |
| | | } |
| | | openSwipeId.value = null |
| | | settingDefaultId.value = item.id |
| | | try { |
| | | await setDefaultVisitor(item.id, item.uuid) |
| | |
| | | } |
| | | finally { |
| | | settingDefaultId.value = null |
| | | } |
| | | } |
| | | |
| | | function handleTouchStart(item: MiniVisitorListItem, event: any) { |
| | | const touch = event.touches?.[0] |
| | | if (!touch) { |
| | | return |
| | | } |
| | | touchingId = item.id |
| | | touchStartX = touch.clientX |
| | | touchStartY = touch.clientY |
| | | } |
| | | |
| | | function handleTouchEnd(item: MiniVisitorListItem, event: any) { |
| | | if (touchingId !== item.id) { |
| | | return |
| | | } |
| | | const touch = event.changedTouches?.[0] |
| | | touchingId = null |
| | | if (!touch) { |
| | | return |
| | | } |
| | | const deltaX = touch.clientX - touchStartX |
| | | const deltaY = touch.clientY - touchStartY |
| | | if (Math.abs(deltaX) <= Math.abs(deltaY) + 10) { |
| | | return |
| | | } |
| | | if (deltaX < -40) { |
| | | openSwipeId.value = item.id |
| | | } else if (deltaX > 20) { |
| | | openSwipeId.value = null |
| | | } |
| | | } |
| | | |
| | | function confirmDelete(item: MiniVisitorListItem) { |
| | | if (deletingId.value) { |
| | | return |
| | | } |
| | | uni.showModal({ |
| | | title: '删除确认', |
| | | content: `确认删除来访人「${displayName(item)}」?`, |
| | | confirmText: '删除', |
| | | confirmColor: '#FF4D4F', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | void handleDelete(item) |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | async function handleDelete(item: MiniVisitorListItem) { |
| | | deletingId.value = item.id |
| | | try { |
| | | await deleteVisitor(item.id, item.uuid) |
| | | openSwipeId.value = null |
| | | uni.showToast({ title: '已删除', icon: 'none' }) |
| | | await loadVisitors() |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '删除来访人失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | deletingId.value = null |
| | | } |
| | | } |
| | | |
| | |
| | | <view v-else class="relative z-10 px-[24rpx] pb-[200rpx] mt-[5vh]"> |
| | | <view class="w-full flex flex-col gap-[16rpx]"> |
| | | <view |
| | | v-for="(item, index) in visitors" |
| | | v-for="item in visitors" |
| | | :key="item.id" |
| | | class="relative flex items-center justify-between px-[24rpx] pt-[24rpx] pb-[64rpx] bg-white rounded-[8rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)]" |
| | | class="relative overflow-hidden rounded-[8rpx] bg-[#FF4D4F]" |
| | | > |
| | | <view class="flex items-center w-[160rpx] shrink-0"> |
| | | <text class="text-[28rpx] text-[#333333] font-medium truncate">{{ displayName(item) }}</text> |
| | | </view> |
| | | |
| | | <view class="flex flex-col justify-center flex-1 mx-[20rpx] min-w-0"> |
| | | <view v-if="showGender || showIdCard" class="flex items-center"> |
| | | <image :src="item.gender === '女' ? appointmentIcons.female : appointmentIcons.male" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate"> |
| | | {{ showIdCard ? (item.idCardMasked || '-') : '-' }} |
| | | </text> |
| | | </view> |
| | | <view v-if="showCompany" class="flex items-center mt-[8rpx]"> |
| | | <image :src="appointmentIcons.company" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate">{{ item.visitorCompany || '-' }}</text> |
| | | </view> |
| | | <text v-if="item.missingFieldCodes?.length" class="mt-[8rpx] text-[22rpx] text-[#F5222D] truncate"> |
| | | 缺失:{{ formatMissingFields(item.missingFieldCodes) }} |
| | | </text> |
| | | </view> |
| | | |
| | | <view class="w-[40rpx] h-[40rpx] shrink-0 flex items-center justify-center" @click.stop="handleEdit(item)"> |
| | | <image :src="`${choosePersonAssetBase}/edit.png`" class="w-[33rpx] h-[33rpx]" /> |
| | | <view class="absolute right-0 top-0 bottom-0 w-[132rpx] bg-[#FF4D4F] flex items-center justify-center z-0" @click.stop="confirmDelete(item)"> |
| | | <text class="text-[28rpx] text-white">{{ deletingId === item.id ? '删除中' : '删除' }}</text> |
| | | </view> |
| | | |
| | | <view |
| | | class="absolute right-[24rpx] bottom-[18rpx] h-[36rpx] px-[18rpx] rounded-full flex items-center justify-center" |
| | | :class="item.isDefault ? 'bg-[#E6F0FF]' : 'bg-[#F5F7FA]'" |
| | | @click.stop="handleSetDefault(item)" |
| | | class="relative z-10 flex items-center justify-between px-[24rpx] pt-[24rpx] pb-[64rpx] bg-white rounded-[8rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] transition-transform duration-200" |
| | | :style="{ transform: openSwipeId === item.id ? 'translateX(-132rpx)' : 'translateX(0)' }" |
| | | @touchstart="handleTouchStart(item, $event)" |
| | | @touchend="handleTouchEnd(item, $event)" |
| | | > |
| | | <text class="text-[22rpx]" :class="item.isDefault ? 'text-[#0068FF]' : 'text-[#999999]'"> |
| | | {{ item.isDefault ? '默认来访人' : (settingDefaultId === item.id ? '设置中...' : '设为默认') }} |
| | | </text> |
| | | <view class="flex items-center w-[160rpx] shrink-0"> |
| | | <text class="text-[28rpx] text-[#333333] font-medium truncate">{{ displayName(item) }}</text> |
| | | </view> |
| | | |
| | | <view class="flex flex-col justify-center flex-1 mx-[20rpx] min-w-0"> |
| | | <view v-if="showGender || showIdCard" class="flex items-center"> |
| | | <image :src="item.gender === '女' ? appointmentIcons.female : appointmentIcons.male" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate"> |
| | | {{ showIdCard ? (item.idCardMasked || '-') : '-' }} |
| | | </text> |
| | | </view> |
| | | <view v-if="showCompany" class="flex items-center mt-[8rpx]"> |
| | | <image :src="appointmentIcons.company" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate">{{ item.visitorCompany || '-' }}</text> |
| | | </view> |
| | | <text v-if="item.missingFieldCodes?.length" class="mt-[8rpx] text-[22rpx] text-[#F5222D] truncate"> |
| | | 缺失:{{ formatMissingFields(item.missingFieldCodes) }} |
| | | </text> |
| | | </view> |
| | | |
| | | <view class="w-[40rpx] h-[40rpx] shrink-0 flex items-center justify-center" @click.stop="handleEdit(item)"> |
| | | <image :src="`${choosePersonAssetBase}/edit.png`" class="w-[33rpx] h-[33rpx]" /> |
| | | </view> |
| | | |
| | | <view |
| | | class="absolute right-[24rpx] bottom-[18rpx] h-[36rpx] px-[18rpx] rounded-full flex items-center justify-center" |
| | | :class="item.isDefault ? 'bg-[#E6F0FF]' : 'bg-[#F5F7FA]'" |
| | | @click.stop="handleSetDefault(item)" |
| | | > |
| | | <text class="text-[22rpx]" :class="item.isDefault ? 'text-[#0068FF]' : 'text-[#999999]'"> |
| | | {{ item.isDefault ? '默认来访人' : (settingDefaultId === item.id ? '设置中...' : '设为默认') }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | |
| | | <view class="mt-[48rpx] flex justify-center pb-[32rpx]"> |
| | | <text class="text-[24rpx] text-[#999999]"> |
| | | 备案号:渝ICP备16005911号-2 |
| | | 备案号:湘ICP备2026025199号-1X |
| | | </text> |
| | | </view> |
| | | </view> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="px-[32rpx] pb-[24rpx] flex-1 min-h-0"> |
| | | <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-full"> |
| | | <view v-if="selectedAreaIds.length > 0" class="flex flex-wrap gap-[16rpx] pb-[16rpx]"> |
| | | <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)"> |
| | |
| | | </view> |
| | | |
| | | <!-- 底部确认按钮 --> |
| | | <view class="px-[32rpx] pb-[68rpx] shrink-0"> |
| | | <view class="px-[32rpx] pt-[12rpx] pb-[68rpx] bg-white shrink-0"> |
| | | <view class="w-full h-[88rpx] rounded-[44rpx] bg-[#0068FF] text-white text-[32rpx] font-medium flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)] active:opacity-80 transition-opacity" @click="confirmRegionPicker">确认</view> |
| | | </view> |
| | | </view> |
| | |
| | | import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest' |
| | | |
| | | export default defineManifestConfig({ |
| | | 'name': 'wot-starter', |
| | | 'appid': 'wxe91f1af7638aa5dd', |
| | | 'name': 'reservation', |
| | | 'appid': 'wxd710abda41dee55d', |
| | | 'description': '', |
| | | 'versionName': '1.0.0', |
| | | 'versionCode': '100', |
| | |
| | | optimization: { |
| | | subPackages: true, |
| | | }, |
| | | appid: '', |
| | | appid: 'wxd710abda41dee55d', |
| | | setting: { |
| | | urlCheck: false, |
| | | }, |
| | |
| | | // 导航栏配置 |
| | | navigationBarBackgroundColor: '@navBgColor', |
| | | navigationBarTextStyle: '@navTxtStyle', |
| | | navigationBarTitleText: 'Wot Starter', |
| | | navigationBarTitleText: 'reservation', |
| | | |
| | | // 页面背景配置 |
| | | backgroundColor: '@bgColor', |
| | |
| | | const response = await alovaInstance.Patch<ApiResult<T>>(url, data, config).send() |
| | | return unwrapApiResult(response, '请求失败') |
| | | } |
| | | |
| | | export async function deleteResult<T>( |
| | | url: string, |
| | | config?: Parameters<typeof alovaInstance.Delete>[2], |
| | | ): Promise<T> { |
| | | const response = await alovaInstance.Delete<ApiResult<T>>(url, undefined, config).send() |
| | | return unwrapApiResult(response, '请求失败') |
| | | } |
| | |
| | | import { getResult, patchResult, postResult, putResult } from './core/request' |
| | | import { deleteResult, getResult, patchResult, postResult, putResult } from './core/request' |
| | | import type { |
| | | MiniVisitorDetailItem, |
| | | MiniVisitorListItem, |
| | |
| | | params: { uuid }, |
| | | }) |
| | | } |
| | | |
| | | export function deleteVisitor(id: number, uuid: string) { |
| | | return deleteResult<void>(`/api/mini/visitors/${id}`, { |
| | | params: { uuid }, |
| | | }) |
| | | } |
| | |
| | | const defineComponent: typeof import('vue')['defineComponent'] |
| | | const defineStore: typeof import('pinia')['defineStore'] |
| | | const deleteMyAppointment: typeof import('./api/appointment')['deleteMyAppointment'] |
| | | const deleteVisitor: typeof import('./api/visitor')['deleteVisitor'] |
| | | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] |
| | | const effectScope: typeof import('vue')['effectScope'] |
| | | const extendRef: typeof import('@vueuse/core')['extendRef'] |
| | |
| | | readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']> |
| | | readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']> |
| | | readonly deleteMyAppointment: UnwrapRef<typeof import('./api/appointment')['deleteMyAppointment']> |
| | | readonly deleteVisitor: UnwrapRef<typeof import('./api/visitor')['deleteVisitor']> |
| | | readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']> |
| | | readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> |
| | | readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']> |
| | |
| | | { |
| | | "name": "wot-starter", |
| | | "appid": "wxe91f1af7638aa5dd", |
| | | "name": "reservation", |
| | | "appid": "wxd710abda41dee55d", |
| | | "description": "", |
| | | "versionName": "1.0.0", |
| | | "versionCode": "100", |
| | |
| | | }, |
| | | "quickapp": {}, |
| | | "mp-weixin": { |
| | | "appid": "", |
| | | "appid": "wxd710abda41dee55d", |
| | | "setting": { |
| | | "urlCheck": false |
| | | }, |
| | |
| | | "globalStyle": { |
| | | "navigationBarBackgroundColor": "@navBgColor", |
| | | "navigationBarTextStyle": "@navTxtStyle", |
| | | "navigationBarTitleText": "Wot Starter", |
| | | "navigationBarTitleText": "reservation", |
| | | "backgroundColor": "@bgColor", |
| | | "backgroundTextStyle": "@bgTxtStyle", |
| | | "backgroundColorTop": "@bgColorTop", |
| | |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | } |
| | |
| | | const defaultPlateTag = '新能源' |
| | | const VISIT_TIME_MODE_SINGLE = 'SINGLE' |
| | | const VISIT_TIME_MODE_MULTI = 'MULTI' |
| | | const APPROVAL_RESULT_SUBSCRIBE_TEMPLATE_ID = '4FQovCBNFM7rth42AWHTusHajO788eT9VpplSN4ZG2s' |
| | | const APPROVAL_RESULT_SUBSCRIBE_TEMPLATE_ID = 'rIJpfN51_q818RTpJAqTe5ssTwWCHnxDM5rpq64uuKM' |
| | | const TIME_SLOT_LABEL_TO_CODE = { |
| | | '全天00:00-23:59': 'ALL_DAY', |
| | | '上午00:00-11:59': 'AM', |
| | |
| | | <script setup lang="ts"> |
| | | import { fetchMiniFieldConfigs } from '@/api/lookup' |
| | | import { fetchMyVisitors, setDefaultVisitor } from '@/api/visitor' |
| | | import { deleteVisitor, fetchMyVisitors, setDefaultVisitor } from '@/api/visitor' |
| | | import { FIELD_CODES, formatVisitorFieldLabels } from '@/constants/fieldCodes' |
| | | import type { MiniFieldConfigItem } from '@/types/lookup' |
| | | import type { MiniVisitorListItem } from '@/types/visitor' |
| | |
| | | const visitors = ref<MiniVisitorListItem[]>([]) |
| | | const loading = ref(false) |
| | | const settingDefaultId = ref<number | null>(null) |
| | | const deletingId = ref<number | null>(null) |
| | | const openSwipeId = ref<number | null>(null) |
| | | const fieldConfigs = ref<MiniFieldConfigItem[]>([]) |
| | | |
| | | let touchStartX = 0 |
| | | let touchStartY = 0 |
| | | let touchingId: number | null = null |
| | | |
| | | const showName = computed(() => isVisible(FIELD_CODES.VISITOR_NAME)) |
| | | const showGender = computed(() => isVisible(FIELD_CODES.GENDER)) |
| | |
| | | } |
| | | |
| | | function handleEdit(item: MiniVisitorListItem) { |
| | | openSwipeId.value = null |
| | | uni.navigateTo({ |
| | | url: `/pages/addPerson/index?id=${item.id}&uuid=${item.uuid}`, |
| | | }) |
| | |
| | | if (item.isDefault || settingDefaultId.value) { |
| | | return |
| | | } |
| | | openSwipeId.value = null |
| | | settingDefaultId.value = item.id |
| | | try { |
| | | await setDefaultVisitor(item.id, item.uuid) |
| | |
| | | } |
| | | finally { |
| | | settingDefaultId.value = null |
| | | } |
| | | } |
| | | |
| | | function handleTouchStart(item: MiniVisitorListItem, event: any) { |
| | | const touch = event.touches?.[0] |
| | | if (!touch) { |
| | | return |
| | | } |
| | | touchingId = item.id |
| | | touchStartX = touch.clientX |
| | | touchStartY = touch.clientY |
| | | } |
| | | |
| | | function handleTouchEnd(item: MiniVisitorListItem, event: any) { |
| | | if (touchingId !== item.id) { |
| | | return |
| | | } |
| | | const touch = event.changedTouches?.[0] |
| | | touchingId = null |
| | | if (!touch) { |
| | | return |
| | | } |
| | | const deltaX = touch.clientX - touchStartX |
| | | const deltaY = touch.clientY - touchStartY |
| | | if (Math.abs(deltaX) <= Math.abs(deltaY) + 10) { |
| | | return |
| | | } |
| | | if (deltaX < -40) { |
| | | openSwipeId.value = item.id |
| | | } else if (deltaX > 20) { |
| | | openSwipeId.value = null |
| | | } |
| | | } |
| | | |
| | | function confirmDelete(item: MiniVisitorListItem) { |
| | | if (deletingId.value) { |
| | | return |
| | | } |
| | | uni.showModal({ |
| | | title: '删除确认', |
| | | content: `确认删除来访人「${displayName(item)}」?`, |
| | | confirmText: '删除', |
| | | confirmColor: '#FF4D4F', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | void handleDelete(item) |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | async function handleDelete(item: MiniVisitorListItem) { |
| | | deletingId.value = item.id |
| | | try { |
| | | await deleteVisitor(item.id, item.uuid) |
| | | openSwipeId.value = null |
| | | uni.showToast({ title: '已删除', icon: 'none' }) |
| | | await loadVisitors() |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '删除来访人失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | deletingId.value = null |
| | | } |
| | | } |
| | | |
| | |
| | | <view v-else class="relative z-10 px-[24rpx] pb-[200rpx] mt-[5vh]"> |
| | | <view class="w-full flex flex-col gap-[16rpx]"> |
| | | <view |
| | | v-for="(item, index) in visitors" |
| | | v-for="item in visitors" |
| | | :key="item.id" |
| | | class="relative flex items-center justify-between px-[24rpx] pt-[24rpx] pb-[64rpx] bg-white rounded-[8rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)]" |
| | | class="relative overflow-hidden rounded-[8rpx] bg-[#FF4D4F]" |
| | | > |
| | | <view class="flex items-center w-[160rpx] shrink-0"> |
| | | <text class="text-[28rpx] text-[#333333] font-medium truncate">{{ displayName(item) }}</text> |
| | | </view> |
| | | |
| | | <view class="flex flex-col justify-center flex-1 mx-[20rpx] min-w-0"> |
| | | <view v-if="showGender || showIdCard" class="flex items-center"> |
| | | <image :src="item.gender === '女' ? appointmentIcons.female : appointmentIcons.male" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate"> |
| | | {{ showIdCard ? (item.idCardMasked || '-') : '-' }} |
| | | </text> |
| | | </view> |
| | | <view v-if="showCompany" class="flex items-center mt-[8rpx]"> |
| | | <image :src="appointmentIcons.company" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate">{{ item.visitorCompany || '-' }}</text> |
| | | </view> |
| | | <text v-if="item.missingFieldCodes?.length" class="mt-[8rpx] text-[22rpx] text-[#F5222D] truncate"> |
| | | 缺失:{{ formatMissingFields(item.missingFieldCodes) }} |
| | | </text> |
| | | </view> |
| | | |
| | | <view class="w-[40rpx] h-[40rpx] shrink-0 flex items-center justify-center" @click.stop="handleEdit(item)"> |
| | | <image :src="`${choosePersonAssetBase}/edit.png`" class="w-[33rpx] h-[33rpx]" /> |
| | | <view class="absolute right-0 top-0 bottom-0 w-[132rpx] bg-[#FF4D4F] flex items-center justify-center z-0" @click.stop="confirmDelete(item)"> |
| | | <text class="text-[28rpx] text-white">{{ deletingId === item.id ? '删除中' : '删除' }}</text> |
| | | </view> |
| | | |
| | | <view |
| | | class="absolute right-[24rpx] bottom-[18rpx] h-[36rpx] px-[18rpx] rounded-full flex items-center justify-center" |
| | | :class="item.isDefault ? 'bg-[#E6F0FF]' : 'bg-[#F5F7FA]'" |
| | | @click.stop="handleSetDefault(item)" |
| | | class="relative z-10 flex items-center justify-between px-[24rpx] pt-[24rpx] pb-[64rpx] bg-white rounded-[8rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] transition-transform duration-200" |
| | | :style="{ transform: openSwipeId === item.id ? 'translateX(-132rpx)' : 'translateX(0)' }" |
| | | @touchstart="handleTouchStart(item, $event)" |
| | | @touchend="handleTouchEnd(item, $event)" |
| | | > |
| | | <text class="text-[22rpx]" :class="item.isDefault ? 'text-[#0068FF]' : 'text-[#999999]'"> |
| | | {{ item.isDefault ? '默认来访人' : (settingDefaultId === item.id ? '设置中...' : '设为默认') }} |
| | | </text> |
| | | <view class="flex items-center w-[160rpx] shrink-0"> |
| | | <text class="text-[28rpx] text-[#333333] font-medium truncate">{{ displayName(item) }}</text> |
| | | </view> |
| | | |
| | | <view class="flex flex-col justify-center flex-1 mx-[20rpx] min-w-0"> |
| | | <view v-if="showGender || showIdCard" class="flex items-center"> |
| | | <image :src="item.gender === '女' ? appointmentIcons.female : appointmentIcons.male" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate"> |
| | | {{ showIdCard ? (item.idCardMasked || '-') : '-' }} |
| | | </text> |
| | | </view> |
| | | <view v-if="showCompany" class="flex items-center mt-[8rpx]"> |
| | | <image :src="appointmentIcons.company" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate">{{ item.visitorCompany || '-' }}</text> |
| | | </view> |
| | | <text v-if="item.missingFieldCodes?.length" class="mt-[8rpx] text-[22rpx] text-[#F5222D] truncate"> |
| | | 缺失:{{ formatMissingFields(item.missingFieldCodes) }} |
| | | </text> |
| | | </view> |
| | | |
| | | <view class="w-[40rpx] h-[40rpx] shrink-0 flex items-center justify-center" @click.stop="handleEdit(item)"> |
| | | <image :src="`${choosePersonAssetBase}/edit.png`" class="w-[33rpx] h-[33rpx]" /> |
| | | </view> |
| | | |
| | | <view |
| | | class="absolute right-[24rpx] bottom-[18rpx] h-[36rpx] px-[18rpx] rounded-full flex items-center justify-center" |
| | | :class="item.isDefault ? 'bg-[#E6F0FF]' : 'bg-[#F5F7FA]'" |
| | | @click.stop="handleSetDefault(item)" |
| | | > |
| | | <text class="text-[22rpx]" :class="item.isDefault ? 'text-[#0068FF]' : 'text-[#999999]'"> |
| | | {{ item.isDefault ? '默认来访人' : (settingDefaultId === item.id ? '设置中...' : '设为默认') }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | |
| | | <view class="mt-[48rpx] flex justify-center pb-[32rpx]"> |
| | | <text class="text-[24rpx] text-[#999999]"> |
| | | 备案号:渝ICP备16005911号-2 |
| | | 备案号:湘ICP备2026025199号-1X |
| | | </text> |
| | | </view> |
| | | </view> |
| | |
| | | const submitting = ref(false) |
| | | const needPhoneAuthorize = ref(false) |
| | | const EMPLOYEE_SUBSCRIBE_TEMPLATE_IDS = [ |
| | | 'BX49OBHvRcoYiCw0ZLqx04BiWQ9KaEQwuVth_Lh_M2A', |
| | | 'GC1S3T2qHCbz8ow130u7elRczuqW6Z8q54mKRCYE-xo', |
| | | 'QlNwG99WXWztRDgtlLcE0fHxfY2VwaX8PRMrwGpEync', |
| | | 'Wip0ODJgybZpeFpcvev_0GK1wQXEqZw-LHOjS-UyDLw', |
| | | ] |
| | | const MINI_PHONE_AUTH_REQUIRED = 10008 |
| | | |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="px-[32rpx] pb-[24rpx] flex-1 min-h-0"> |
| | | <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-full"> |
| | | <view v-if="selectedAreaIds.length > 0" class="flex flex-wrap gap-[16rpx] pb-[16rpx]"> |
| | | <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)"> |
| | |
| | | </view> |
| | | |
| | | <!-- 底部确认按钮 --> |
| | | <view class="px-[32rpx] pb-[68rpx] shrink-0"> |
| | | <view class="px-[32rpx] pt-[12rpx] pb-[68rpx] bg-white shrink-0"> |
| | | <view class="w-full h-[88rpx] rounded-[44rpx] bg-[#0068FF] text-white text-[32rpx] font-medium flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)] active:opacity-80 transition-opacity" @click="confirmRegionPicker">确认</view> |
| | | </view> |
| | | </view> |
| | |
| | | import { useMiniAuthStore } from '@/store/auth' |
| | | |
| | | export const EMPLOYEE_SUBSCRIBE_TEMPLATE_IDS = [ |
| | | 'BX49OBHvRcoYiCw0ZLqx04BiWQ9KaEQwuVth_Lh_M2A', |
| | | 'GC1S3T2qHCbz8ow130u7elRczuqW6Z8q54mKRCYE-xo', |
| | | 'QlNwG99WXWztRDgtlLcE0fHxfY2VwaX8PRMrwGpEync', |
| | | 'Wip0ODJgybZpeFpcvev_0GK1wQXEqZw-LHOjS-UyDLw', |
| | | ] |
| | | |
| | | export const MINI_PHONE_AUTH_REQUIRED = 10008 |