From 8665daa7aa863f96d0d8f27c167edae1efc38022 Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期五, 31 七月 2026 14:46:19 +0800
Subject: [PATCH] Merge branch 'master' into dev
---
ruoyi-admin/src/main/resources/application-test.yml | 2
ruoyi-ui/src/views/visitor-admin/record/visitor.vue | 2
ruoyi-system/src/test/java/com/ruoyi/system/utils/ProtocolHtmlSanitizerTest.java | 43 +
doc/sql/20260729_t_visitor_config_add_appointment_default_visit_time.sql | 3
ruoyi-ui/src/utils/visitor-admin.js | 24
ruoyi-ui/src/assets/styles/visitor-admin.scss | 383 +++++++++++--
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysProtocolConfigServiceImpl.java | 3
ruoyi-system/src/main/resources/mapper/system/SysDashboardMapper.xml | 6
ruoyi-ui/src/api/visitor-admin/version.js | 18
ruoyi-ui/src/assets/icons/svg/config-sign-confirm.svg | 1
ruoyi-ui/src/views/visitor-admin/system/version.vue | 155 +++++
ruoyi-ui/src/views/visitor-admin/dashboard/index.vue | 126 ++++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorConfigServiceImpl.java | 12
ruoyi-ui/src/assets/icons/svg/config-face-capture.svg | 1
ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVersionUpdateVO.java | 2
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVersionUpdateController.java | 42 +
ruoyi-ui/src/assets/icons/svg/config-system.svg | 1
ruoyi-ui/src/assets/icons/svg/config-appointment-register.svg | 1
doc/sql/20260706_t_visitor_config.sql | 5
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDashboardMapper.java | 2
ruoyi-ui/src/assets/icons/svg/config-device.svg | 1
ruoyi-admin/src/main/resources/application-prod.yml | 2
ruoyi-system/src/main/java/com/ruoyi/system/utils/ProtocolHtmlSanitizer.java | 141 +++++
doc/sql/20260723_visitor_record_add_export_permission.sql | 29 +
ruoyi-ui/src/views/visitor-admin/system/protocol.vue | 180 ++++++
ruoyi-ui/src/assets/icons/svg/config-id-card.svg | 1
visitor-admin-ui/css/version-update.css | 31
ruoyi-ui/src/assets/icons/svg/config-face-compare.svg | 1
ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml | 23
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorConfigSaveRequest.java | 8
visitor-admin-ui/js/version-update.js | 36 +
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java | 29
ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml | 5
ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java | 2
ruoyi-ui/src/assets/icons/svg/config-appointment-time.svg | 1
ruoyi-ui/src/assets/icons/svg/config-onsite-register.svg | 1
doc/sql/20260723_t_visitor_machine_add_fingerprint.sql | 3
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/MiddlePlatformDataQuery.java | 6
ruoyi-ui/src/assets/icons/svg/config-register.svg | 1
ruoyi-ui/src/views/visitor-admin/device/index.vue | 8
ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVisitorConfigVO.java | 5
ruoyi-ui/src/assets/icons/svg/config-receiver-confirm.svg | 1
ruoyi-system/src/main/resources/mapper/system/SysVisitorMachineMapper.xml | 5
ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml | 3
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java | 2
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDashboardServiceImpl.java | 3
ruoyi-ui/src/assets/icons/svg/config-print.svg | 1
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorConfig.java | 5
ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml | 5
visitor-admin-ui/pages/version-update.html | 6
ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java | 19
ruoyi-ui/src/views/visitor-admin/system/config.vue | 106 ++-
52 files changed, 1,280 insertions(+), 222 deletions(-)
diff --git a/doc/sql/20260706_t_visitor_config.sql b/doc/sql/20260706_t_visitor_config.sql
index 85a4635..54488fd 100644
--- a/doc/sql/20260706_t_visitor_config.sql
+++ b/doc/sql/20260706_t_visitor_config.sql
@@ -3,10 +3,11 @@
`config_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '配置主键',
`onsite_registration_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '现场登记开关:0关闭 1开启',
`appointment_registration_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '预约登记开关:0关闭 1开启',
+ `appointment_default_visit_time` varchar(5) DEFAULT NULL COMMENT '预约登记默认访问时间,格式HH:mm',
`register_receiver_confirm_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '登记需接待人确认:0关闭 1开启',
`sign_receiver_confirm_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '签离需接待人确认:0关闭 1开启',
`face_compare_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '人像对比开关:0关闭 1开启',
- `face_capture_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '全方位人脸采集开关:0关闭 1开启',
+ `face_capture_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '人脸采集开关:0关闭 1开启',
`manual_id_card_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '允许手动输入身份证:0关闭 1开启',
`print_fields` varchar(500) NOT NULL DEFAULT 'visitorName,idCard,appointmentTimeRange,interviewee' COMMENT '凭条打印字段,英文逗号分隔并按顺序打印',
`system_name` varchar(100) NOT NULL DEFAULT '自主访客登记管理平台' COMMENT '系统名称',
@@ -27,6 +28,7 @@
`config_id`,
`onsite_registration_enabled`,
`appointment_registration_enabled`,
+ `appointment_default_visit_time`,
`register_receiver_confirm_enabled`,
`sign_receiver_confirm_enabled`,
`face_compare_enabled`,
@@ -43,6 +45,7 @@
1,
1,
1,
+ NULL,
1,
1,
1,
diff --git a/doc/sql/20260723_t_visitor_machine_add_fingerprint.sql b/doc/sql/20260723_t_visitor_machine_add_fingerprint.sql
new file mode 100644
index 0000000..3d38bfb
--- /dev/null
+++ b/doc/sql/20260723_t_visitor_machine_add_fingerprint.sql
@@ -0,0 +1,3 @@
+-- 访客机管理:补充百度SDK指纹字段
+ALTER TABLE `t_visitor_machine`
+ ADD COLUMN `fingerprint` varchar(255) DEFAULT NULL COMMENT '百度SDK指纹' AFTER `online_status`;
diff --git a/doc/sql/20260723_visitor_record_add_export_permission.sql b/doc/sql/20260723_visitor_record_add_export_permission.sql
new file mode 100644
index 0000000..eee095d
--- /dev/null
+++ b/doc/sql/20260723_visitor_record_add_export_permission.sql
@@ -0,0 +1,29 @@
+-- 访客登记记录新增导出按钮权限
+INSERT INTO sys_menu (
+ menu_id, menu_name, parent_id, order_num, path, component, query,
+ is_frame, is_cache, menu_type, visible, status, perms, icon,
+ create_by, create_time, remark
+)
+SELECT
+ (SELECT COALESCE(MAX(menu_id), 0) + 1 FROM sys_menu menu_id_source),
+ '访客记录导出', parent.menu_id, 2, '#', NULL, NULL,
+ '1', '1', 'F', '0', '0', 'visitor:record:export', '#',
+ 'admin', NOW(), '访客后台按钮权限'
+FROM sys_menu parent
+WHERE parent.perms = 'visitor:record:view'
+ AND NOT EXISTS (
+ SELECT 1 FROM sys_menu existing WHERE existing.perms = 'visitor:record:export'
+ );
+
+-- 默认授权给若依管理员角色,普通角色可在角色管理中按需授权。
+INSERT INTO sys_role_menu (role_id, menu_id)
+SELECT r.role_id, m.menu_id
+FROM sys_role r
+JOIN sys_menu m ON m.perms = 'visitor:record:export'
+WHERE r.role_id = 1
+ AND NOT EXISTS (
+ SELECT 1
+ FROM sys_role_menu rm
+ WHERE rm.role_id = r.role_id
+ AND rm.menu_id = m.menu_id
+ );
diff --git a/doc/sql/20260729_t_visitor_config_add_appointment_default_visit_time.sql b/doc/sql/20260729_t_visitor_config_add_appointment_default_visit_time.sql
new file mode 100644
index 0000000..5e540ab
--- /dev/null
+++ b/doc/sql/20260729_t_visitor_config_add_appointment_default_visit_time.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `t_visitor_config`
+ ADD COLUMN `appointment_default_visit_time` varchar(5) DEFAULT NULL COMMENT '预约登记默认访问时间,格式HH:mm'
+ AFTER `appointment_registration_enabled`;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVersionUpdateController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVersionUpdateController.java
index 28c5f40..1504dc4 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVersionUpdateController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVersionUpdateController.java
@@ -1,9 +1,13 @@
package com.ruoyi.web.controller.api;
import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import com.ruoyi.common.utils.file.FileUtils;
+import com.ruoyi.framework.config.ServerConfig;
import com.ruoyi.system.dto.SysVersionUpdateAddRequest;
import com.ruoyi.system.dto.SysVersionUpdateDeleteRequest;
import com.ruoyi.system.dto.SysVersionUpdateDetailRequest;
@@ -15,6 +19,7 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
/**
* 版本更新接口
@@ -25,8 +30,15 @@
@RequestMapping("/system/version/update")
public class SysVersionUpdateController extends BaseController
{
+ private static final long MAX_PACKAGE_SIZE = 200L * 1024 * 1024;
+
+ private static final String[] ALLOWED_PACKAGE_EXTENSION = { "apk" };
+
@Autowired
private ISysVersionUpdateService versionUpdateService;
+
+ @Autowired
+ private ServerConfig serverConfig;
/**
* 分页查询版本更新列表
@@ -61,6 +73,36 @@
return toAjax(versionUpdateService.add(request, getUsername()));
}
+ @Log(title = "版本更新", businessType = BusinessType.INSERT)
+ @PreAuthorize("@ss.hasPermi('visitor:version:add')")
+ @PostMapping("/upload")
+ public AjaxResult upload(MultipartFile file)
+ {
+ if (file == null || file.isEmpty())
+ {
+ return error("请上传安装包");
+ }
+ if (file.getSize() >= MAX_PACKAGE_SIZE)
+ {
+ return error("安装包大小必须小于200M");
+ }
+ try
+ {
+ String fileName = FileUploadUtils.upload(RuoYiConfig.getUploadPath(), file,
+ ALLOWED_PACKAGE_EXTENSION, MAX_PACKAGE_SIZE);
+ AjaxResult ajax = success();
+ ajax.put("url", serverConfig.getUrl() + fileName);
+ ajax.put("fileName", fileName);
+ ajax.put("newFileName", FileUtils.getName(fileName));
+ ajax.put("originalFilename", file.getOriginalFilename());
+ return ajax;
+ }
+ catch (Exception e)
+ {
+ return error(e.getMessage());
+ }
+ }
+
/**
* 删除版本更新
*/
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
index b718a50..ed30b01 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
@@ -154,7 +154,7 @@
}
if (deptService.checkDeptExistUser(deptId))
{
- return warn("部门存在用户,不允许删除");
+ return warn("操作失败:存在员工");
}
deptService.checkDeptDataScope(deptId);
return AjaxResult.success(deptService.deleteDeptById(deptId));
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/MiddlePlatformDataQuery.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/MiddlePlatformDataQuery.java
index 2e5fb1d..a32e2d2 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/MiddlePlatformDataQuery.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/MiddlePlatformDataQuery.java
@@ -463,10 +463,10 @@
}
/**
- * 目的:将中台 1/0 在线状态转换为前端展示使用的 online/offline。
+ * 目的:将中台 1/0 在线状态转换为本地数据库使用的在线状态枚举。
*/
private static String toLocalOnlineStatus(Object value) {
- // 本地页面使用 online/offline,中台设备接口使用 1/0。
- return "1".equals(String.valueOf(value)) || "online".equalsIgnoreCase(String.valueOf(value)) ? "online" : "offline";
+ return "1".equals(String.valueOf(value)) || "online".equalsIgnoreCase(String.valueOf(value))
+ ? "ONLINE" : "OFFLINE";
}
}
diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml
index c5e0b47..028f161 100644
--- a/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ruoyi-admin/src/main/resources/application-prod.yml
@@ -192,7 +192,7 @@
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
- excludes: /system/notice
+ excludes: /system/notice,/system/protocol/config/save
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# file upload
diff --git a/ruoyi-admin/src/main/resources/application-test.yml b/ruoyi-admin/src/main/resources/application-test.yml
index e26e741..eccc799 100644
--- a/ruoyi-admin/src/main/resources/application-test.yml
+++ b/ruoyi-admin/src/main/resources/application-test.yml
@@ -185,7 +185,7 @@
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
- excludes: /system/notice
+ excludes: /system/notice,/system/protocol/config/save
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# file upload
diff --git a/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
index 990d0f7..bd86ef2 100644
--- a/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
+++ b/ruoyi-applet/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
@@ -118,7 +118,7 @@
}
if (deptService.checkDeptExistUser(deptId))
{
- return warn("部门存在用户,不允许删除");
+ return warn("操作失败:存在员工");
}
deptService.checkDeptDataScope(deptId);
return AjaxResult.success(deptService.deleteDeptById(deptId));
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java
index d9f2b13..5ff593f 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java
@@ -102,13 +102,20 @@
throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
InvalidExtensionException
{
+ return upload(baseDir, file, allowedExtension, DEFAULT_MAX_SIZE);
+ }
+
+ public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension, long maxSize)
+ throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
+ InvalidExtensionException
+ {
int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
{
throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
}
- assertAllowed(file, allowedExtension);
+ assertAllowed(file, allowedExtension, maxSize);
String fileName = extractFilename(file);
@@ -158,10 +165,16 @@
public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
throws FileSizeLimitExceededException, InvalidExtensionException
{
+ assertAllowed(file, allowedExtension, DEFAULT_MAX_SIZE);
+ }
+
+ public static final void assertAllowed(MultipartFile file, String[] allowedExtension, long maxSize)
+ throws FileSizeLimitExceededException, InvalidExtensionException
+ {
long size = file.getSize();
- if (size > DEFAULT_MAX_SIZE)
+ if (size > maxSize)
{
- throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
+ throw new FileSizeLimitExceededException(maxSize / 1024 / 1024);
}
String fileName = file.getOriginalFilename();
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorConfig.java
index fa8ed96..b0c1e18 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorConfig.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorConfig.java
@@ -24,6 +24,9 @@
/** 预约登记开关 */
private Boolean appointmentRegistrationEnabled;
+ /** 预约登记默认访问时间,格式HH:mm */
+ private String appointmentDefaultVisitTime;
+
/** 登记需接待人确认 */
private Boolean registerReceiverConfirmEnabled;
@@ -33,7 +36,7 @@
/** 人像对比开关 */
private Boolean faceCompareEnabled;
- /** 全方位人脸采集开关 */
+ /** 人脸采集开关 */
private Boolean faceCaptureEnabled;
/** 允许手动输入身份证 */
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorConfigSaveRequest.java b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorConfigSaveRequest.java
index 4bbeba3..b062caf 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorConfigSaveRequest.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorConfigSaveRequest.java
@@ -21,6 +21,10 @@
@ApiModelProperty("是否允许预约登记")
private Boolean appointmentRegistrationEnabled;
+ /** 预约登记默认访问时间 */
+ @ApiModelProperty("预约登记默认访问时间,格式HH:mm")
+ private String appointmentDefaultVisitTime;
+
/** 登记是否需要接待人确认 */
@ApiModelProperty("登记是否需要接待人确认")
private Boolean registerReceiverConfirmEnabled;
@@ -33,8 +37,8 @@
@ApiModelProperty("是否启用人像对比")
private Boolean faceCompareEnabled;
- /** 是否启用全方位人脸采集 */
- @ApiModelProperty("是否启用全方位人脸采集")
+ /** 是否启用人脸采集 */
+ @ApiModelProperty("是否启用人脸采集")
private Boolean faceCaptureEnabled;
/** 是否允许手动输入身份证 */
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDashboardMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDashboardMapper.java
index 52c3bc0..2cdb55c 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDashboardMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDashboardMapper.java
@@ -21,7 +21,7 @@
@Param("beginTime") Date beginTime,
@Param("endTime") Date endTime);
- public SysDashboardStatusSummaryVO selectStatusSummary();
+ public SysDashboardStatusSummaryVO selectStatusSummary(Date beginTime, Date endTime);
public List<SysDashboardTrendDailyVO> selectTrendDaily(@Param("beginTime") Date beginTime,
@Param("endTime") Date endTime);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDashboardServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDashboardServiceImpl.java
index fe539d4..8ff02fb 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDashboardServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDashboardServiceImpl.java
@@ -61,7 +61,8 @@
dashboardMapper.selectRegisterSummary("APPOINTMENT", range.getBeginTime(), range.getEndTime())));
overview.setOnsiteAppointment(defaultRegisterSummary(
dashboardMapper.selectRegisterSummary("ONSITE", range.getBeginTime(), range.getEndTime())));
- overview.setStatusSummary(defaultStatusSummary(dashboardMapper.selectStatusSummary()));
+ overview.setStatusSummary(defaultStatusSummary(
+ dashboardMapper.selectStatusSummary(range.getBeginTime(), range.getEndTime())));
overview.setTrend(buildTrend(range, dashboardMapper.selectTrendDaily(range.getBeginTime(), range.getEndTime())));
return overview;
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysProtocolConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysProtocolConfigServiceImpl.java
index 1260f36..b85cdbe 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysProtocolConfigServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysProtocolConfigServiceImpl.java
@@ -7,6 +7,7 @@
import com.ruoyi.system.dto.SysProtocolConfigSaveRequest;
import com.ruoyi.system.mapper.SysProtocolConfigMapper;
import com.ruoyi.system.service.ISysProtocolConfigService;
+import com.ruoyi.system.utils.ProtocolHtmlSanitizer;
import com.ruoyi.system.vo.SysProtocolConfigVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -45,7 +46,7 @@
{
throw new ServiceException("协议名称不能为空");
}
- String protocolContent = StringUtils.trim(request.getProtocolContent());
+ String protocolContent = StringUtils.trim(ProtocolHtmlSanitizer.clean(request.getProtocolContent()));
if (StringUtils.isEmpty(protocolContent))
{
throw new ServiceException("协议内容不能为空");
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java
index 0f1fb7e..d31000a 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java
@@ -16,6 +16,7 @@
import org.springframework.util.CollectionUtils;
import java.util.List;
+import java.util.Objects;
/**
* 版本更新服务实现
@@ -34,6 +35,12 @@
SysVersionUpdatePageRequest query = request == null ? new SysVersionUpdatePageRequest() : request;
PageInfo<SysVersionUpdateVO> pageInfo = new PageInfo<SysVersionUpdateVO>(query.getPageNum(), query.getPageSize());
List<SysVersionUpdateVO> list = versionUpdateMapper.pageList(query, pageInfo);
+ SysVersionUpdateVO latest = versionUpdateMapper.selectLatest();
+ Long latestUpdateId = latest == null ? null : latest.getUpdateId();
+ for (SysVersionUpdateVO versionUpdate : list)
+ {
+ versionUpdate.setLatest(Objects.equals(versionUpdate.getUpdateId(), latestUpdateId));
+ }
pageInfo.setRecords(list);
return pageInfo;
}
@@ -41,7 +48,12 @@
@Override
public SysVersionUpdateVO latest()
{
- return versionUpdateMapper.selectLatest();
+ SysVersionUpdateVO latest = versionUpdateMapper.selectLatest();
+ if (latest != null)
+ {
+ latest.setLatest(Boolean.TRUE);
+ }
+ return latest;
}
@Override
@@ -56,6 +68,8 @@
{
throw new ServiceException("版本更新记录不存在");
}
+ SysVersionUpdateVO latest = versionUpdateMapper.selectLatest();
+ detail.setLatest(latest != null && Objects.equals(detail.getUpdateId(), latest.getUpdateId()));
return detail;
}
@@ -71,11 +85,13 @@
{
throw new ServiceException("版本号不能为空");
}
- String updateContent = StringUtils.trim(request.getUpdateContent());
- if (StringUtils.isEmpty(updateContent))
+ String versionNumber = versionNo.replaceFirst("^[vV]", "");
+ if (!versionNumber.matches("\\d+(\\.\\d+)*"))
{
- throw new ServiceException("更新内容不能为空");
+ throw new ServiceException("版本号格式不正确,请输入数字或使用小数点分隔");
}
+ versionNo = versionNumber;
+ String updateContent = StringUtils.trim(request.getUpdateContent());
String packageUrl = StringUtils.trim(request.getPackageUrl());
if (StringUtils.isEmpty(packageUrl))
{
@@ -99,6 +115,11 @@
{
throw new ServiceException("更新主键集合不能为空");
}
+ SysVersionUpdateVO latest = versionUpdateMapper.selectLatest();
+ if (latest != null && request.getUpdateIds().contains(latest.getUpdateId()))
+ {
+ throw new ServiceException("最新版本不能删除");
+ }
return versionUpdateMapper.deleteByIds(request.getUpdateIds());
}
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorConfigServiceImpl.java
index 4789ad8..ec2bd80 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorConfigServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorConfigServiceImpl.java
@@ -93,6 +93,16 @@
{
config.setAppointmentRegistrationEnabled(request.getAppointmentRegistrationEnabled());
}
+ if (request.getAppointmentDefaultVisitTime() != null)
+ {
+ String appointmentDefaultVisitTime = StringUtils.trim(request.getAppointmentDefaultVisitTime());
+ if (StringUtils.isNotEmpty(appointmentDefaultVisitTime)
+ && !appointmentDefaultVisitTime.matches("([01]\\d|2[0-3]):[0-5]\\d"))
+ {
+ throw new ServiceException("预约登记默认访问时间格式不正确");
+ }
+ config.setAppointmentDefaultVisitTime(appointmentDefaultVisitTime);
+ }
if (request.getRegisterReceiverConfirmEnabled() != null)
{
config.setRegisterReceiverConfirmEnabled(request.getRegisterReceiverConfirmEnabled());
@@ -181,6 +191,7 @@
SysVisitorConfig config = new SysVisitorConfig();
config.setOnsiteRegistrationEnabled(true);
config.setAppointmentRegistrationEnabled(true);
+ config.setAppointmentDefaultVisitTime("");
config.setRegisterReceiverConfirmEnabled(true);
config.setSignReceiverConfirmEnabled(true);
config.setFaceCompareEnabled(true);
@@ -200,6 +211,7 @@
vo.setConfigId(config.getConfigId());
vo.setOnsiteRegistrationEnabled(config.getOnsiteRegistrationEnabled());
vo.setAppointmentRegistrationEnabled(config.getAppointmentRegistrationEnabled());
+ vo.setAppointmentDefaultVisitTime(config.getAppointmentDefaultVisitTime());
vo.setRegisterReceiverConfirmEnabled(config.getRegisterReceiverConfirmEnabled());
vo.setSignReceiverConfirmEnabled(config.getSignReceiverConfirmEnabled());
vo.setFaceCompareEnabled(config.getFaceCompareEnabled());
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/utils/ProtocolHtmlSanitizer.java b/ruoyi-system/src/main/java/com/ruoyi/system/utils/ProtocolHtmlSanitizer.java
new file mode 100644
index 0000000..2cb1ce6
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/utils/ProtocolHtmlSanitizer.java
@@ -0,0 +1,141 @@
+package com.ruoyi.system.utils;
+
+import com.ruoyi.common.utils.html.HTMLFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 协议富文本白名单清洗工具。
+ *
+ * @author ruoyi
+ */
+public final class ProtocolHtmlSanitizer
+{
+ private static final Pattern DANGEROUS_ELEMENT_PATTERN = Pattern.compile(
+ "<(script|style|iframe|object|embed|link|meta)[^>]*>.*?</\\1\\s*>",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+
+ private static final Pattern DANGEROUS_SINGLE_ELEMENT_PATTERN = Pattern.compile(
+ "<(script|style|iframe|object|embed|link|meta)[^>]*?/?>",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+
+ private static final Pattern STYLE_ATTRIBUTE_PATTERN = Pattern.compile(
+ "\\sstyle\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s>]+))",
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern SAFE_COLOR_PATTERN = Pattern.compile("#[0-9a-fA-F]{3,8}");
+ private static final Pattern SAFE_FONT_SIZE_PATTERN = Pattern.compile(
+ "(?:xx-small|x-small|small|medium|large|x-large|xx-large|[1-9][0-9]{0,2}px)",
+ Pattern.CASE_INSENSITIVE);
+ private static final Pattern SAFE_FONT_WEIGHT_PATTERN = Pattern.compile("(?:bold|normal|[1-9]00)", Pattern.CASE_INSENSITIVE);
+ private static final Pattern SAFE_FONT_STYLE_PATTERN = Pattern.compile("(?:italic|normal)", Pattern.CASE_INSENSITIVE);
+
+ private ProtocolHtmlSanitizer()
+ {
+ }
+
+ /**
+ * 仅保留协议编辑器支持的安全 HTML 标签和格式。
+ *
+ * @param html 原始协议 HTML
+ * @return 清洗后的协议 HTML
+ */
+ public static String clean(String html)
+ {
+ if (html == null)
+ {
+ return null;
+ }
+ String content = DANGEROUS_ELEMENT_PATTERN.matcher(html).replaceAll("");
+ content = DANGEROUS_SINGLE_ELEMENT_PATTERN.matcher(content).replaceAll("");
+ content = sanitizeStyleAttributes(content);
+ return createFilter().filter(content).replace("\\\"", "\"");
+ }
+
+ private static String sanitizeStyleAttributes(String html)
+ {
+ Matcher matcher = STYLE_ATTRIBUTE_PATTERN.matcher(html);
+ StringBuffer result = new StringBuffer();
+ while (matcher.find())
+ {
+ String style = matcher.group(2) != null ? matcher.group(2)
+ : matcher.group(3) != null ? matcher.group(3) : matcher.group(4);
+ String safeStyle = sanitizeStyle(style);
+ String replacement = safeStyle.isEmpty() ? "" : " style=\"" + safeStyle + "\"";
+ matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
+ }
+ matcher.appendTail(result);
+ return result.toString();
+ }
+
+ private static String sanitizeStyle(String style)
+ {
+ List<String> safeDeclarations = new ArrayList<>();
+ for (String declaration : style.split(";"))
+ {
+ String[] parts = declaration.split(":", 2);
+ if (parts.length != 2)
+ {
+ continue;
+ }
+ String property = parts[0].trim().toLowerCase();
+ String value = parts[1].trim();
+ if ("text-align".equals(property) && Arrays.asList("left", "center", "right", "justify").contains(value.toLowerCase()))
+ {
+ safeDeclarations.add("text-align: " + value.toLowerCase());
+ }
+ else if ("color".equals(property) && SAFE_COLOR_PATTERN.matcher(value).matches())
+ {
+ safeDeclarations.add("color: " + value.toLowerCase());
+ }
+ else if ("font-size".equals(property) && SAFE_FONT_SIZE_PATTERN.matcher(value).matches())
+ {
+ safeDeclarations.add("font-size: " + value.toLowerCase());
+ }
+ else if ("font-weight".equals(property) && SAFE_FONT_WEIGHT_PATTERN.matcher(value).matches())
+ {
+ safeDeclarations.add("font-weight: " + value.toLowerCase());
+ }
+ else if ("font-style".equals(property) && SAFE_FONT_STYLE_PATTERN.matcher(value).matches())
+ {
+ safeDeclarations.add("font-style: " + value.toLowerCase());
+ }
+ }
+ return String.join("; ", safeDeclarations);
+ }
+
+ private static HTMLFilter createFilter()
+ {
+ HashMap<String, List<String>> allowed = new HashMap<>();
+ List<String> styleAttribute = Collections.singletonList("style");
+ for (String tag : Arrays.asList("div", "p", "span", "ul", "ol", "li", "blockquote", "pre", "h1", "h2", "h3", "h4", "h5", "h6"))
+ {
+ allowed.put(tag, styleAttribute);
+ }
+ for (String tag : Arrays.asList("strong", "b", "em", "i", "br"))
+ {
+ allowed.put(tag, Collections.emptyList());
+ }
+
+ Map<String, Object> config = new HashMap<>();
+ config.put("vAllowed", allowed);
+ config.put("vSelfClosingTags", new String[] { "br" });
+ config.put("vNeedClosingTags", new String[] { "div", "p", "span", "ul", "ol", "li", "blockquote", "pre",
+ "h1", "h2", "h3", "h4", "h5", "h6", "strong", "b", "em", "i" });
+ config.put("vDisallowed", new String[] {});
+ config.put("vAllowedProtocols", new String[] {});
+ config.put("vProtocolAtts", new String[] {});
+ config.put("vRemoveBlanks", new String[] {});
+ config.put("vAllowedEntities", new String[] { "amp", "gt", "lt", "quot", "nbsp" });
+ config.put("stripComment", true);
+ config.put("encodeQuotes", false);
+ config.put("alwaysMakeTags", false);
+ return new HTMLFilter(config);
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVersionUpdateVO.java b/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVersionUpdateVO.java
index 5238162..3915d97 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVersionUpdateVO.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVersionUpdateVO.java
@@ -25,6 +25,8 @@
private Boolean forceUpdate;
+ private Boolean latest;
+
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVisitorConfigVO.java b/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVisitorConfigVO.java
index ed559fa..641f14f 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVisitorConfigVO.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVisitorConfigVO.java
@@ -24,6 +24,9 @@
@ApiModelProperty(value = "预约登记开关", example = "true", allowableValues = "true,false")
private Boolean appointmentRegistrationEnabled;
+ @ApiModelProperty(value = "预约登记默认访问时间", example = "18:00")
+ private String appointmentDefaultVisitTime;
+
@ApiModelProperty(value = "登记需接待人确认", example = "true", allowableValues = "true,false")
private Boolean registerReceiverConfirmEnabled;
@@ -33,7 +36,7 @@
@ApiModelProperty(value = "人像对比开关", example = "true", allowableValues = "true,false")
private Boolean faceCompareEnabled;
- @ApiModelProperty(value = "全方位人脸采集开关", example = "true", allowableValues = "true,false")
+ @ApiModelProperty(value = "人脸采集开关", example = "true", allowableValues = "true,false")
private Boolean faceCaptureEnabled;
@ApiModelProperty(value = "允许手动输入身份证", example = "true", allowableValues = "true,false")
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDashboardMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDashboardMapper.xml
index 1c8efef..d9c0526 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysDashboardMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysDashboardMapper.xml
@@ -17,7 +17,11 @@
<select id="selectStatusSummary" resultType="SysDashboardStatusSummaryVO">
select sum(case when status = 'VISITING' then 1 else 0 end) as visitingCount,
- sum(case when status in ('SIGNED_OUT', 'OVERDUE_AUTO_SIGNED_OUT') then 1 else 0 end) as signedOutCount,
+ sum(case when status in ('SIGNED_OUT', 'OVERDUE_AUTO_SIGNED_OUT')
+ and sign_time is not null
+ and sign_time >= #{param1}
+ and sign_time < #{param2}
+ then 1 else 0 end) as signedOutCount,
sum(case when status = 'PENDING_CONFIRM' then 1 else 0 end) as overdueCount
from t_visitor_record
where del_flag = '0'
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml
index cf439f6..c09db51 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysDeptMapper.xml
@@ -66,7 +66,8 @@
</select>
<select id="checkDeptExistUser" parameterType="Long" resultType="int">
- select count(1) from sys_user where dept_id = #{deptId} and del_flag = '0'
+ select (select count(1) from sys_user where dept_id = #{deptId} and del_flag = '0')
+ + (select count(1) from t_employee where dept_id = #{deptId} and del_flag = '0')
</select>
<select id="hasChildByDeptId" parameterType="Long" resultType="int">
@@ -156,4 +157,4 @@
update sys_dept set del_flag = '2' where dept_id = #{deptId}
</delete>
-</mapper>
\ No newline at end of file
+</mapper>
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml
index ef60b58..bc2cd67 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysOperLogMapper.xml
@@ -94,8 +94,7 @@
from sys_oper_log sol
<where>
<if test="query != null and query.operName != null and query.operName != ''">
- and (sol.oper_name like concat('%', #{query.operName}, '%')
- or sol.nickName like concat('%', #{query.operName}, '%'))
+ and sol.oper_name like concat('%', #{query.operName}, '%')
</if>
<if test="query != null and query.title != null and query.title != ''">
and sol.title like concat('%', #{query.title}, '%')
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml
index 9e95e2f..9a03f30 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml
@@ -8,6 +8,7 @@
<id property="configId" column="config_id" />
<result property="onsiteRegistrationEnabled" column="onsite_registration_enabled" />
<result property="appointmentRegistrationEnabled" column="appointment_registration_enabled" />
+ <result property="appointmentDefaultVisitTime" column="appointment_default_visit_time" />
<result property="registerReceiverConfirmEnabled" column="register_receiver_confirm_enabled" />
<result property="signReceiverConfirmEnabled" column="sign_receiver_confirm_enabled" />
<result property="faceCompareEnabled" column="face_compare_enabled" />
@@ -31,6 +32,7 @@
select config_id,
onsite_registration_enabled,
appointment_registration_enabled,
+ appointment_default_visit_time,
register_receiver_confirm_enabled,
sign_receiver_confirm_enabled,
face_compare_enabled,
@@ -62,6 +64,7 @@
insert into t_visitor_config (
onsite_registration_enabled,
appointment_registration_enabled,
+ appointment_default_visit_time,
register_receiver_confirm_enabled,
sign_receiver_confirm_enabled,
face_compare_enabled,
@@ -79,6 +82,7 @@
) values (
#{onsiteRegistrationEnabled},
#{appointmentRegistrationEnabled},
+ #{appointmentDefaultVisitTime},
#{registerReceiverConfirmEnabled},
#{signReceiverConfirmEnabled},
#{faceCompareEnabled},
@@ -100,6 +104,7 @@
update t_visitor_config
set onsite_registration_enabled = #{onsiteRegistrationEnabled},
appointment_registration_enabled = #{appointmentRegistrationEnabled},
+ appointment_default_visit_time = #{appointmentDefaultVisitTime},
register_receiver_confirm_enabled = #{registerReceiverConfirmEnabled},
sign_receiver_confirm_enabled = #{signReceiverConfirmEnabled},
face_compare_enabled = #{faceCompareEnabled},
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysVisitorMachineMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysVisitorMachineMapper.xml
index 89d55c5..0a36cfa 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysVisitorMachineMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysVisitorMachineMapper.xml
@@ -158,6 +158,9 @@
<if test="deviceId != null and deviceId != ''">
device_id = #{deviceId},
</if>
+ <if test="onlineStatus != null and onlineStatus != ''">
+ online_status = #{onlineStatus},
+ </if>
<if test="enabled != null">
enabled = #{enabled}, <!-- 注意:enabled 可能是 Boolean 或 Integer,若允许 false 或 0,则只判 null -->
</if>
@@ -175,7 +178,7 @@
update_time = sysdate()
</set>
where machine_id = #{machineId}
- and del_flag = '0'
+ and del_flag = '0'
</update>
<update id="updateEnabled">
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml
index 4fa43d4..0fe2613 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml
@@ -65,11 +65,30 @@
id_card,
register_type,
interviewee_name,
- interviewee_dept,
+ coalesce(
+ (
+ select d.dept_name
+ from t_employee e
+ inner join sys_dept d on d.dept_id = e.dept_id and d.del_flag = '0'
+ where e.employee_name = t_visitor_record.interviewee_name
+ and e.del_flag = '0'
+ order by e.employee_id
+ limit 1
+ ),
+ nullif(interviewee_dept, '')
+ ) as interviewee_dept,
receiver_name,
receiver_verify_method,
visit_area_ids,
- visit_area_names,
+ coalesce(
+ (
+ select group_concat(a.area_name order by find_in_set(a.area_id, t_visitor_record.visit_area_ids) separator ',')
+ from t_visit_area a
+ where a.del_flag = '0'
+ and find_in_set(a.area_id, t_visitor_record.visit_area_ids)
+ ),
+ nullif(visit_area_names, '')
+ ) as visit_area_names,
plate_no,
visit_reason,
valid_start_time,
diff --git a/ruoyi-system/src/test/java/com/ruoyi/system/utils/ProtocolHtmlSanitizerTest.java b/ruoyi-system/src/test/java/com/ruoyi/system/utils/ProtocolHtmlSanitizerTest.java
new file mode 100644
index 0000000..59fa9d3
--- /dev/null
+++ b/ruoyi-system/src/test/java/com/ruoyi/system/utils/ProtocolHtmlSanitizerTest.java
@@ -0,0 +1,43 @@
+package com.ruoyi.system.utils;
+
+public class ProtocolHtmlSanitizerTest
+{
+ public static void main(String[] args)
+ {
+ shouldKeepSupportedFormatting();
+ shouldRemoveDangerousContentAndAttributes();
+ }
+
+ private static void shouldKeepSupportedFormatting()
+ {
+ String html = "<div style=\"text-align: center; position: fixed\">入园承诺</div>\n"
+ + "<div><span style=\"font-weight: bold; color: #111827\">第二行</span></div>";
+
+ String result = ProtocolHtmlSanitizer.clean(html);
+
+ require(result.contains("<div style=\"text-align: center\">入园承诺</div>"), "居中样式未保留");
+ require(result.contains("<span style=\"font-weight: bold; color: #111827\">第二行</span>"), "文字样式未保留");
+ require(!result.contains("position"), "非白名单样式未清除");
+ }
+
+ private static void shouldRemoveDangerousContentAndAttributes()
+ {
+ String html = "<div onclick=\"alert(1)\">正文<script>alert(1)</script>"
+ + "<span style=\"background-image: url(javascript:alert(1))\">内容</span></div>";
+
+ String result = ProtocolHtmlSanitizer.clean(html);
+
+ require(result.contains("<div>正文<span>内容</span></div>"), "安全正文结构未保留");
+ require(!result.contains("script"), "危险标签未清除");
+ require(!result.contains("onclick"), "事件属性未清除");
+ require(!result.contains("javascript"), "危险样式内容未清除");
+ }
+
+ private static void require(boolean condition, String message)
+ {
+ if (!condition)
+ {
+ throw new AssertionError(message);
+ }
+ }
+}
diff --git a/ruoyi-ui/src/api/visitor-admin/version.js b/ruoyi-ui/src/api/visitor-admin/version.js
index f071f43..6c7d78e 100644
--- a/ruoyi-ui/src/api/visitor-admin/version.js
+++ b/ruoyi-ui/src/api/visitor-admin/version.js
@@ -5,14 +5,26 @@
}
export function getVersion(updateId) {
- return request({ url: '/system/version/update/detail', method: 'post', data: { updateId } })
+ return request({ url: '/system/version/update/detail', method: 'post', data: { updateId }})
}
export function addVersion(data) {
return request({ url: '/system/version/update/add', method: 'post', data })
}
-export function delVersion(updateIds) {
- return request({ url: '/system/version/update/delete', method: 'post', data: { updateIds } })
+export function uploadVersionPackage(data) {
+ return request({
+ url: '/system/version/update/upload',
+ method: 'post',
+ data,
+ timeout: 120000,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ repeatSubmit: false
+ }
+ })
}
+export function delVersion(updateIds) {
+ return request({ url: '/system/version/update/delete', method: 'post', data: { updateIds }})
+}
diff --git a/ruoyi-ui/src/assets/icons/svg/config-appointment-register.svg b/ruoyi-ui/src/assets/icons/svg/config-appointment-register.svg
new file mode 100644
index 0000000..3d81da6
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-appointment-register.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 2.5v3M16 2.5v3M3 8.5h18"/><circle cx="12" cy="14" r="3.2"/><path d="M12 12.2V14l1.2.8"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-appointment-time.svg b/ruoyi-ui/src/assets/icons/svg/config-appointment-time.svg
new file mode 100644
index 0000000..2ef2b0e
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-appointment-time.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-device.svg b/ruoyi-ui/src/assets/icons/svg/config-device.svg
new file mode 100644
index 0000000..0cf70d5
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-device.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v10"/><path d="m8 9 4 4 4-4"/><path d="M5 17a7 7 0 0 0 14 0"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-face-capture.svg b/ruoyi-ui/src/assets/icons/svg/config-face-capture.svg
new file mode 100644
index 0000000..0ce89f7
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-face-capture.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/><path d="M8.5 11v-1M15.5 11v-1M9 16c1.8 1.2 4.2 1.2 6 0M8 7.5c1-.7 2.3-1 4-1s3 .3 4 1"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-face-compare.svg b/ruoyi-ui/src/assets/icons/svg/config-face-compare.svg
new file mode 100644
index 0000000..6e4bbf3
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-face-compare.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H4a1 1 0 0 0-1 1v4M16 3h4a1 1 0 0 1 1 1v4M8 21H4a1 1 0 0 1-1-1v-4M16 21h4a1 1 0 0 0 1-1v-4"/><circle cx="12" cy="10" r="3"/><path d="M7.5 18c.7-3 2.2-4.5 4.5-4.5s3.8 1.5 4.5 4.5"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-id-card.svg b/ruoyi-ui/src/assets/icons/svg/config-id-card.svg
new file mode 100644
index 0000000..12b9382
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-id-card.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="5" width="19" height="14" rx="2"/><circle cx="8" cy="11" r="2.2"/><path d="M4.8 16c.5-2 1.6-3 3.2-3s2.7 1 3.2 3M14 10h4.5M14 14h4.5"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-onsite-register.svg b/ruoyi-ui/src/assets/icons/svg/config-onsite-register.svg
new file mode 100644
index 0000000..53526db
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-onsite-register.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 2.5v3M16 2.5v3M3 8.5h18M8.5 14l2.2 2.2 4.8-5"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-print.svg b/ruoyi-ui/src/assets/icons/svg/config-print.svg
new file mode 100644
index 0000000..a17040b
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-print.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 8V3.5h10V8M7 17H5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M7 14h10v6.5H7zM17.5 11h.01"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-receiver-confirm.svg b/ruoyi-ui/src/assets/icons/svg/config-receiver-confirm.svg
new file mode 100644
index 0000000..25d2183
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-receiver-confirm.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="7" r="3"/><path d="M3.5 19c.4-4 2.2-6 5.5-6 2 0 3.5.7 4.4 2"/><path d="m15 17 2 2 4-5"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-register.svg b/ruoyi-ui/src/assets/icons/svg/config-register.svg
new file mode 100644
index 0000000..e892b43
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-register.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3.5h10a2 2 0 0 1 2 2v15H5v-15a2 2 0 0 1 2-2Z"/><path d="M9 3.5v3h6v-3M8.5 11h7M8.5 15h5"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-sign-confirm.svg b/ruoyi-ui/src/assets/icons/svg/config-sign-confirm.svg
new file mode 100644
index 0000000..8801bf7
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-sign-confirm.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="7" r="3"/><path d="M3.5 19c.4-4 2.2-6 5.5-6 2 0 3.5.7 4.4 2M14 18h7M18 14l4 4-4 4"/></svg>
diff --git a/ruoyi-ui/src/assets/icons/svg/config-system.svg b/ruoyi-ui/src/assets/icons/svg/config-system.svg
new file mode 100644
index 0000000..8976ee4
--- /dev/null
+++ b/ruoyi-ui/src/assets/icons/svg/config-system.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.86 2.86-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21h-4v-.1A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.86-2.86.06-.06A1.7 1.7 0 0 0 4.2 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H2.4v-4h.1A1.7 1.7 0 0 0 4.2 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06L6.66 3.8l.06.06A1.7 1.7 0 0 0 8.6 4.2a1.7 1.7 0 0 0 1-.6A1.7 1.7 0 0 0 10 2.5v-.1h4v.1a1.7 1.7 0 0 0 1 1.7 1.7 1.7 0 0 0 1.88-.34l.06-.06 2.86 2.86-.06.06A1.7 1.7 0 0 0 19.4 8.6a1.7 1.7 0 0 0 .6 1c.3.26.69.4 1.1.4h.1v4h-.1a1.7 1.7 0 0 0-1.7 1Z"/></svg>
diff --git a/ruoyi-ui/src/assets/styles/visitor-admin.scss b/ruoyi-ui/src/assets/styles/visitor-admin.scss
index d06c309..36b3ca8 100644
--- a/ruoyi-ui/src/assets/styles/visitor-admin.scss
+++ b/ruoyi-ui/src/assets/styles/visitor-admin.scss
@@ -416,6 +416,11 @@
background: #fff2df;
}
+ &.unactivated {
+ color: #708195;
+ background: #eef2f6;
+ }
+
&.danger {
color: var(--visitor-danger);
background: #fff3f3;
@@ -781,6 +786,7 @@
border-radius: 18px;
background: linear-gradient(180deg, rgba(248, 252, 255, 0.96), rgba(239, 247, 255, 0.62));
box-shadow: inset 0 0 0 1px rgba(222, 233, 250, 0.72);
+ cursor: pointer;
}
.chart-legend {
@@ -822,41 +828,72 @@
.quick-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px;
+ gap: 14px 16px;
padding: 18px 0 0;
}
.quick-entry {
- height: 52px;
+ height: 64px;
display: grid;
- grid-template-columns: 38px 1fr;
+ grid-template-columns: 42px 1fr;
align-items: center;
- gap: 12px;
- padding: 0 14px;
+ gap: 18px;
+ padding: 0 20px;
border: 1px solid #dbe8ff;
border-radius: 12px;
color: #19304f;
background: #fff;
- font-size: 14px;
+ font-size: 16px;
font-weight: 800;
cursor: pointer;
- transition: transform 0.16s ease, box-shadow 0.16s ease;
+ outline: none;
+ transition: transform 0.16s ease, border-color 0.16s ease, color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
}
.quick-entry:hover {
transform: translateY(-1px);
- box-shadow: 0 10px 24px rgba(47, 103, 255, 0.12);
+ border-color: #8bb4ff;
+ color: #3974e8;
+ background: linear-gradient(90deg, rgba(111, 163, 255, 0.12), rgba(111, 163, 255, 0.03));
+ box-shadow: 0 10px 24px rgba(47, 103, 255, 0.14), inset 0 0 0 1px rgba(111, 163, 255, 0.08);
+ }
+
+ .quick-entry:hover .quick-icon {
+ background: linear-gradient(145deg, #8db8ff, #6497f4);
+ box-shadow: 0 8px 18px rgba(79, 137, 241, 0.28), inset 0 0 0 1px rgba(255, 255, 255, 0.36);
+ }
+
+ .quick-entry.active,
+ .quick-entry:active,
+ .quick-entry:focus-visible {
+ border-color: #a86cff;
+ color: #7146e8;
+ background: linear-gradient(90deg, rgba(168, 108, 255, 0.12), rgba(168, 108, 255, 0.03));
+ box-shadow: 0 8px 20px rgba(137, 83, 255, 0.14), inset 0 0 0 1px rgba(168, 108, 255, 0.12);
+ }
+
+ .quick-entry.active .quick-icon,
+ .quick-entry:active .quick-icon,
+ .quick-entry:focus-visible .quick-icon {
+ background: linear-gradient(135deg, #8f5bff, #bd8cff);
+ box-shadow: 0 8px 18px rgba(143, 91, 255, 0.28);
}
.quick-icon {
- width: 34px;
- height: 34px;
+ position: relative;
+ width: 42px;
+ height: 42px;
display: grid;
place-items: center;
- border-radius: 12px;
+ border-radius: 13px;
color: #fff;
- background: var(--entry-color);
- box-shadow: 0 8px 18px rgba(47, 103, 255, 0.18);
+ background: linear-gradient(145deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)), var(--entry-color);
+ box-shadow: 0 8px 18px rgba(82, 112, 180, 0.22), inset 0 0 0 1px rgba(255, 255, 255, 0.34);
+
+ i {
+ font-size: 19px;
+ font-weight: 500;
+ }
}
@media (max-width: 1280px) {
@@ -904,14 +941,15 @@
grid-template-columns: 190px 1fr;
border: 1px solid rgba(221, 232, 247, 0.92);
border-radius: 14px;
- background: var(--visitor-card);
+ background: #fff;
box-shadow: 0 16px 40px rgba(34, 69, 126, 0.08);
overflow: hidden;
}
-.config-nav {
+.config-card > .config-nav {
padding: 18px 12px;
border-right: 1px solid #eef3f8;
+ background-color: #fff !important;
h1 {
margin: 0 0 28px;
@@ -934,32 +972,26 @@
font-size: 13px;
text-align: left;
cursor: pointer;
+ outline: 0;
&.active {
color: #1781ff;
background: #e6f7ff;
font-weight: 800;
+ box-shadow: none;
}
}
-.nav-folder {
- width: 13px;
- height: 10px;
- display: inline-block;
- border: 1.5px solid currentColor;
- border-top: 0;
- position: relative;
-}
-
-.nav-folder::before {
- content: "";
- position: absolute;
- left: 0;
- top: -5px;
- width: 7px;
- height: 5px;
- border: 1.5px solid currentColor;
- border-bottom: 0;
+.config-nav-icon {
+ width: 16px;
+ height: 16px;
+ flex: 0 0 16px;
+ color: currentColor;
+ fill: none !important;
+ stroke: currentColor;
+ font-size: 15px;
+ text-align: center;
+ vertical-align: middle;
}
.config-workspace {
@@ -973,21 +1005,24 @@
margin-bottom: 28px;
h2 {
- position: relative;
+ min-height: 22px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
margin: 0 0 14px;
- padding-left: 12px;
color: #10213b;
font-size: 15px;
+ line-height: 22px;
font-weight: 800;
}
h2::before {
content: "";
- position: absolute;
- left: 0;
- top: 1px;
width: 4px;
height: 22px;
+ flex: 0 0 4px;
+ align-self: center;
+ border-radius: 2px;
background: #2e8064;
}
@@ -1007,6 +1042,7 @@
border-bottom: 1px solid #edf2f7;
color: #10213b;
font-size: 14px;
+ line-height: 20px;
font-weight: 800;
input {
@@ -1017,6 +1053,9 @@
.row-label {
min-width: 140px;
+ display: inline-flex;
+ align-items: center;
+ align-self: stretch;
}
}
@@ -1024,8 +1063,67 @@
width: 18px;
height: 18px;
flex: 0 0 18px;
- border: 2px solid #3b8267;
- border-radius: 2px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #4c9378;
+ fill: none !important;
+ stroke: currentColor;
+ font-size: 18px;
+ font-weight: 600;
+ vertical-align: middle;
+}
+
+.system-entry .row-icon {
+ width: 16px;
+ height: 16px;
+ flex-basis: 16px;
+}
+
+.appointment-time-row {
+ height: 48px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ border-bottom: 1px solid #edf2f7;
+ color: #10213b;
+ font-size: 14px;
+ line-height: 20px;
+ font-weight: 800;
+
+ .row-label {
+ min-width: 176px;
+ display: inline-flex;
+ align-items: center;
+ align-self: stretch;
+ }
+}
+
+.time-day {
+ color: #26384f;
+ font-weight: 500;
+}
+
+.appointment-time-input {
+ width: 118px !important;
+
+ .el-input__inner {
+ height: 30px;
+ padding: 0 28px 0 10px;
+ border-color: #cfd8e3;
+ color: #26384f;
+ font-size: 13px;
+ line-height: 30px;
+ }
+
+ .el-input__prefix {
+ display: none;
+ }
+
+ .el-input__suffix {
+ right: 6px;
+ line-height: 30px;
+ }
}
.switch-control {
@@ -1064,7 +1162,18 @@
.print-layout {
display: grid;
grid-template-columns: minmax(420px, 1fr) 360px;
+ align-items: start;
gap: 44px;
+}
+
+.print-settings .config-section h2 {
+ height: 20px;
+ min-height: 20px;
+ line-height: 20px;
+}
+
+.print-settings .config-section h2::before {
+ align-self: center;
}
.print-field-list {
@@ -1083,7 +1192,7 @@
border-radius: 6px;
color: #253855;
font-size: 13px;
- cursor: pointer;
+ cursor: grab;
user-select: none;
input {
@@ -1091,29 +1200,19 @@
height: 13px;
margin: 0;
accent-color: #2d725d;
+ cursor: pointer;
}
}
-.print-field-item.sortable-chosen {
+.print-field-chosen {
background: #eef6ff;
box-shadow: 0 6px 16px rgba(31, 88, 160, 0.14);
+ cursor: grabbing;
}
.print-field-ghost {
opacity: 0.45;
background: #f3f8ff;
-}
-
-.print-field-drag {
- width: 16px;
- color: #6f86a3;
- cursor: grab;
- text-align: center;
- touch-action: none;
-}
-
-.print-field-drag:active {
- cursor: grabbing;
}
.sort-hint {
@@ -1130,24 +1229,62 @@
font-size: 12px;
}
+.sort-hint-icon {
+ color: #f6b817;
+ font-size: 14px;
+}
+
+.print-preview {
+ width: 360px;
+ max-width: 100%;
+ align-self: start;
+}
+
.preview-title {
- height: 34px;
+ height: 26px;
display: flex;
align-items: center;
gap: 10px;
color: #111827;
font-size: 14px;
font-weight: 800;
+
+ button {
+ width: 22px;
+ height: 22px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 0;
+ color: #6f747b;
+ background: transparent;
+ font-size: 16px;
+ cursor: pointer;
+ }
+
+ button:hover {
+ color: #2d725d;
+ }
+}
+
+.preview-stage {
+ width: 100%;
+ min-height: 392px;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+ padding: 10px 18px 24px;
+ background: #f0f0f0;
}
.ticket-preview {
width: 290px;
min-height: 365px;
- margin: 36px 0 0 36px;
+ box-sizing: border-box;
padding: 18px 34px 24px;
border: 1px solid #cfd5dd;
background: #fff;
- box-shadow: 0 0 0 36px #f0f0f0;
h3 {
margin: 0 0 18px;
@@ -1204,10 +1341,14 @@
}
.logo-upload {
+ width: 316px;
+ max-width: 100%;
height: 38px;
- display: flex;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 8px;
+ position: relative;
padding: 0 12px;
border: 1px solid #d9e3ef;
border-radius: 20px;
@@ -1215,6 +1356,48 @@
background: #f7faff;
font-size: 12px;
overflow: hidden;
+ cursor: pointer;
+}
+
+.logo-upload input[type="file"] {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ cursor: pointer;
+}
+
+.logo-upload input[type="file"]:disabled {
+ cursor: not-allowed;
+}
+
+.logo-upload-button,
+.logo-file-name {
+ position: relative;
+ z-index: 1;
+ line-height: 1;
+}
+
+.logo-upload-button {
+ height: 24px;
+ display: inline-flex;
+ align-items: center;
+ padding: 0 10px;
+ border: 1px solid #c8d2df;
+ border-radius: 2px;
+ color: #26384f;
+ background: #fff;
+ font-weight: 700;
+}
+
+.logo-file-name {
+ min-width: 0;
+ overflow: hidden;
+ color: #6f7f92;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.config-actions {
@@ -1242,6 +1425,7 @@
.protocol-card {
min-height: calc(100vh - 118px);
+ position: relative;
padding: 18px 26px 82px;
border-radius: 6px;
background: #fff;
@@ -1365,9 +1549,10 @@
&.ordered::before {
left: 0;
top: 2px;
- content: "1\A2\A3";
+ content: "1.\A 2.\A 3.";
white-space: pre;
- font-size: 8px;
+ font-family: Arial, sans-serif;
+ font-size: 7px;
font-weight: 700;
line-height: 8px;
}
@@ -1442,18 +1627,28 @@
.protocol-editor {
min-height: 405px;
+ position: relative;
padding: 14px 8px;
color: #1f2d3d;
background: #fff;
font-size: 14px;
line-height: 1.8;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
outline: 0;
overflow: auto;
- &:empty::before {
+ &.is-empty::before {
content: attr(data-placeholder);
+ position: absolute;
+ top: 14px;
+ left: 8px;
color: #7c8da6;
pointer-events: none;
+ }
+
+ &.is-empty:focus::before {
+ display: none;
}
p {
@@ -1473,9 +1668,9 @@
}
.protocol-actions {
- position: fixed;
- left: 252px;
- bottom: 28px;
+ position: absolute;
+ left: 26px;
+ bottom: 24px;
}
.protocol-save-button {
@@ -1501,9 +1696,6 @@
min-width: 680px;
}
- .protocol-actions {
- left: 238px;
- }
}
.version-page {
@@ -1617,7 +1809,23 @@
.version-form {
display: grid;
gap: 14px;
- padding: 18px 48px 20px 20px;
+ padding: 18px 54px 20px;
+}
+
+.version-create-dialog {
+ .el-dialog__header {
+ padding: 20px 54px 10px;
+ text-align: left;
+ }
+
+ .el-dialog__title {
+ font-size: 14px;
+ font-weight: 700;
+ }
+
+ .el-dialog__body {
+ padding: 0;
+ }
}
.version-row {
@@ -1630,7 +1838,9 @@
> span:first-child {
padding-top: 8px;
- text-align: right;
+ font-size: 12px;
+ font-weight: 600;
+ text-align: left;
}
&.required > span:first-child::before {
@@ -1641,17 +1851,33 @@
}
.version-input-line {
- display: grid;
- grid-template-columns: 18px 1fr;
- align-items: center;
+ position: relative;
+
+ > span {
+ position: absolute;
+ z-index: 1;
+ left: 10px;
+ top: 9px;
+ color: #24364f;
+ line-height: 1;
+ }
+
+ input {
+ padding-left: 25px;
+ }
}
.version-row textarea {
height: 68px;
+ box-sizing: border-box;
+}
+
+.version-upload-field {
+ width: 100%;
}
.upload-drop {
- width: 242px;
+ width: 100%;
height: 102px;
display: grid;
place-items: center;
@@ -1670,9 +1896,16 @@
color: #24364f;
background: #f9fbfe;
}
+
+ &.drag-over {
+ border-color: #2e73ff;
+ color: #2e73ff;
+ background: #f5f9ff;
+ }
}
.upload-tip {
+ width: 100%;
margin: 10px 0 0;
color: #8a9ab0;
font-size: 11px;
@@ -1692,6 +1925,10 @@
input:checked + .switch-control::after {
transform: translateX(26px);
}
+
+ input:checked + .switch-control {
+ background: #22b573;
+ }
}
.area-page,
diff --git a/ruoyi-ui/src/utils/visitor-admin.js b/ruoyi-ui/src/utils/visitor-admin.js
index 77e98e1..3923db7 100644
--- a/ruoyi-ui/src/utils/visitor-admin.js
+++ b/ruoyi-ui/src/utils/visitor-admin.js
@@ -21,7 +21,29 @@
}
export function onlineText(value) {
- return value === 'online' || value === '1' || value === true ? '在线' : '离线'
+ const status = normalizeOnlineStatus(value)
+ if (status === 'ONLINE') {
+ return '在线'
+ }
+ if (status === 'OFFLINE') {
+ return '离线'
+ }
+ return '未激活'
+}
+
+export function onlineStatusClass(value) {
+ return normalizeOnlineStatus(value).toLowerCase()
+}
+
+function normalizeOnlineStatus(value) {
+ const status = String(value === undefined || value === null ? '' : value).trim().toUpperCase()
+ if (status === 'ONLINE' || status === '1' || status === 'TRUE') {
+ return 'ONLINE'
+ }
+ if (status === 'OFFLINE' || status === '0' || status === 'FALSE') {
+ return 'OFFLINE'
+ }
+ return 'UNACTIVATED'
}
export function toIdArray(value) {
diff --git a/ruoyi-ui/src/views/visitor-admin/dashboard/index.vue b/ruoyi-ui/src/views/visitor-admin/dashboard/index.vue
index f4c038e..2fb6c53 100644
--- a/ruoyi-ui/src/views/visitor-admin/dashboard/index.vue
+++ b/ruoyi-ui/src/views/visitor-admin/dashboard/index.vue
@@ -55,7 +55,7 @@
<section class="lower-grid">
<article class="panel trend-panel">
<h2>登记趋势</h2>
- <canvas ref="trendChart" width="760" height="310" aria-label="登记趋势折线图" />
+ <canvas ref="trendChart" width="760" height="310" aria-label="登记趋势折线图" @click="handleTrendChartClick" />
<div class="chart-legend">
<span><i class="legend-dot blue" />现场登记</span>
<span><i class="legend-dot green" />预约登记</span>
@@ -69,9 +69,13 @@
v-for="item in quickEntries"
:key="item.title"
class="quick-entry"
+ :class="{ active: activeQuickEntry === item.path }"
type="button"
:style="{ '--entry-color': item.color }"
- @click="$router.push(item.path)"
+ @mousedown="activeQuickEntry = item.path"
+ @focus="activeQuickEntry = item.path"
+ @blur="activeQuickEntry = ''"
+ @click="handleQuickEntry(item)"
>
<span class="quick-icon"><i :class="item.icon" /></span>
<span>{{ item.title }}</span>
@@ -91,6 +95,8 @@
data() {
return {
activeDateFilter: '最近7天',
+ activeTrendIndex: null,
+ activeQuickEntry: '',
dateRange: [],
dateFilters: ['今日', '最近7天', '最近30天'],
dateFilterMap: {
@@ -146,14 +152,14 @@
}
],
quickEntries: [
- { title: '部门管理', color: '#2580ff', icon: 'el-icon-office-building', path: '/visitor/organization/dept' },
- { title: '员工管理', color: '#ff7545', icon: 'el-icon-user', path: '/visitor/organization/employee' },
- { title: '设备管理', color: '#ff4c5d', icon: 'el-icon-monitor', path: '/visitor/device/machine' },
- { title: '预约记录', color: '#ff9e33', icon: 'el-icon-notebook-2', path: '/visitor/record/list' },
- { title: '区域管理', color: '#43bd19', icon: 'el-icon-location-outline', path: '/visitor/area' },
- { title: '账号管理', color: '#5b77f1', icon: 'el-icon-setting', path: '/visitor/account/admin' },
- { title: '系统配置', color: '#416bea', icon: 'el-icon-s-tools', path: '/visitor/system/config' },
- { title: '系统更新', color: '#22bfc4', icon: 'el-icon-refresh', path: '/visitor/system/version' }
+ { title: '部门管理', color: '#78a7ff', icon: 'el-icon-office-building', path: '/visitor/organization/dept' },
+ { title: '员工管理', color: '#a979f5', icon: 'el-icon-user', path: '/visitor/organization/employee' },
+ { title: '设备管理', color: '#5bd5ad', icon: 'el-icon-mobile-phone', path: '/visitor/device/machine' },
+ { title: '预约记录', color: '#ffad7b', icon: 'el-icon-reading', path: '/visitor/record/list' },
+ { title: '区域管理', color: '#4ccbd5', icon: 'el-icon-map-location', path: '/visitor/area' },
+ { title: '账号管理', color: '#7b88f5', icon: 'el-icon-lock', path: '/visitor/account/admin' },
+ { title: '系统配置', color: '#9aa9bd', icon: 'el-icon-setting', path: '/visitor/system/config' },
+ { title: '系统更新', color: '#6da8f7', icon: 'el-icon-refresh', path: '/visitor/system/version' }
],
chartData: {
labels: ['09-08', '09-09', '09-10', '09-11', '09-12', '09-13', '09-14'],
@@ -286,6 +292,7 @@
appointment: this.normalizeSeries(trend.appointment, labels.length),
vehicle: this.normalizeSeries(trend.vehicle, labels.length)
}
+ this.activeTrendIndex = null
},
// 目的:把趋势序列补齐为指定长度,避免图表绘制时数组缺位。
normalizeSeries(series, length) {
@@ -301,6 +308,32 @@
// 目的:格式化统计数值,空值按 0 展示。
formatCount(value) {
return String(value == null ? 0 : value)
+ },
+ // 目的:记录快捷入口选中态并进入对应管理页面。
+ handleQuickEntry(item) {
+ this.activeQuickEntry = item.path
+ this.$router.push(item.path)
+ },
+ // 目的:根据点击位置选中最近的趋势日期并重绘图表。
+ handleTrendChartClick(event) {
+ const canvas = this.$refs.trendChart
+ const labelCount = this.chartData.labels.length
+ if (!canvas || !labelCount) {
+ return
+ }
+ const rect = canvas.getBoundingClientRect()
+ const dpr = window.devicePixelRatio || 1
+ const logicalWidth = canvas.width / dpr
+ const logicalX = (event.clientX - rect.left) * (logicalWidth / rect.width)
+ const padding = { left: 42, right: 16 }
+ const chartWidth = logicalWidth - padding.left - padding.right
+ if (logicalX < padding.left || logicalX > logicalWidth - padding.right) {
+ return
+ }
+ const divider = Math.max(labelCount - 1, 1)
+ const index = Math.round(((logicalX - padding.left) / chartWidth) * divider)
+ this.activeTrendIndex = Math.max(0, Math.min(labelCount - 1, index))
+ this.drawTrendChart()
},
// 目的:按当前趋势数据重绘 canvas 折线图。
drawTrendChart() {
@@ -327,6 +360,7 @@
const ticks = [0, step, step * 2, step * 3, step * 4, step * 5, max]
ctx.clearRect(0, 0, width, height)
+ this.drawTrendSelection(ctx, padding, chartWidth, chartHeight)
ctx.font = '10px Microsoft YaHei, Arial'
ctx.textAlign = 'right'
ctx.textBaseline = 'middle'
@@ -346,17 +380,71 @@
ctx.textAlign = 'center'
ctx.textBaseline = 'top'
const labelDivider = Math.max(this.chartData.labels.length - 1, 1)
+ const visibleLabelIndexes = this.getTrendLabelIndexes(this.chartData.labels.length, chartWidth)
this.chartData.labels.forEach((label, index) => {
+ if (!visibleLabelIndexes.has(index)) {
+ return
+ }
const x = padding.left + (chartWidth / labelDivider) * index
- ctx.fillStyle = '#9aabc0'
+ ctx.fillStyle = index === this.activeTrendIndex ? '#7b4dff' : '#9aabc0'
+ ctx.font = index === this.activeTrendIndex ? '700 10px Microsoft YaHei, Arial' : '10px Microsoft YaHei, Arial'
ctx.fillText(label, x, height - 20)
})
- this.drawSeries(ctx, this.chartData.onsite, '#3c7cff', padding, chartWidth, chartHeight, max, true)
- this.drawSeries(ctx, this.chartData.appointment, '#23bd8e', padding, chartWidth, chartHeight, max, true)
+ this.drawSeries(ctx, this.chartData.onsite, '#3c7cff', padding, chartWidth, chartHeight, max, true, this.activeTrendIndex)
+ this.drawSeries(ctx, this.chartData.appointment, '#23bd8e', padding, chartWidth, chartHeight, max, true, this.activeTrendIndex)
+ },
+ // 目的:根据图表宽度均匀抽样日期标签,避免长时间范围下横轴文字重叠。
+ getTrendLabelIndexes(labelCount, chartWidth) {
+ const indexes = new Set()
+ if (labelCount <= 0) {
+ return indexes
+ }
+ const maxLabelCount = Math.max(2, Math.floor(chartWidth / 54) + 1)
+ const visibleCount = Math.min(labelCount, maxLabelCount)
+ const divider = Math.max(visibleCount - 1, 1)
+ for (let position = 0; position < visibleCount; position++) {
+ indexes.add(Math.round(((labelCount - 1) * position) / divider))
+ }
+ if (this.activeTrendIndex != null) {
+ indexes.add(this.activeTrendIndex)
+ }
+ return indexes
+ },
+ // 目的:为当前选中日期绘制紫色圆角背景,与快捷入口选中态保持一致。
+ drawTrendSelection(ctx, padding, chartWidth, chartHeight) {
+ if (this.activeTrendIndex == null || !this.chartData.labels.length) {
+ return
+ }
+ const divider = Math.max(this.chartData.labels.length - 1, 1)
+ const spacing = chartWidth / divider
+ const width = Math.min(74, Math.max(42, spacing * 0.72))
+ const x = padding.left + spacing * this.activeTrendIndex - width / 2
+ const y = 10
+ const height = padding.top + chartHeight - y + 4
+ const radius = 14
+
+ ctx.save()
+ ctx.beginPath()
+ ctx.moveTo(x + radius, y)
+ ctx.lineTo(x + width - radius, y)
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
+ ctx.lineTo(x + width, y + height - radius)
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
+ ctx.lineTo(x + radius, y + height)
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius)
+ ctx.lineTo(x, y + radius)
+ ctx.quadraticCurveTo(x, y, x + radius, y)
+ ctx.closePath()
+ ctx.fillStyle = 'rgba(148, 91, 255, 0.08)'
+ ctx.strokeStyle = 'rgba(148, 91, 255, 0.28)'
+ ctx.lineWidth = 1
+ ctx.fill()
+ ctx.stroke()
+ ctx.restore()
},
// 目的:绘制单条趋势线和可选面积填充。
- drawSeries(ctx, data, color, padding, chartWidth, chartHeight, max, fill) {
+ drawSeries(ctx, data, color, padding, chartWidth, chartHeight, max, fill, activeIndex) {
const pointDivider = Math.max(data.length - 1, 1)
const points = data.map((value, index) => ({
x: padding.left + (chartWidth / pointDivider) * index,
@@ -388,10 +476,16 @@
ctx.fill()
}
- points.forEach(point => {
+ points.forEach((point, index) => {
+ if (index === activeIndex) {
+ ctx.fillStyle = '#fff'
+ ctx.beginPath()
+ ctx.arc(point.x, point.y, 5.2, 0, Math.PI * 2)
+ ctx.fill()
+ }
ctx.fillStyle = color
ctx.beginPath()
- ctx.arc(point.x, point.y, 2.2, 0, Math.PI * 2)
+ ctx.arc(point.x, point.y, index === activeIndex ? 3.6 : 2.2, 0, Math.PI * 2)
ctx.fill()
})
}
diff --git a/ruoyi-ui/src/views/visitor-admin/device/index.vue b/ruoyi-ui/src/views/visitor-admin/device/index.vue
index 0f6f4dd..ec69942 100644
--- a/ruoyi-ui/src/views/visitor-admin/device/index.vue
+++ b/ruoyi-ui/src/views/visitor-admin/device/index.vue
@@ -69,7 +69,7 @@
</el-table-column>
<el-table-column label="在线状态" align="center" width="100">
<template slot-scope="scope">
- <span class="status-pill" :class="scope.row.onlineStatus === 'online' ? 'online' : 'offline'">{{ onlineLabel(scope.row.onlineStatus) }}</span>
+ <span class="status-pill" :class="onlineClass(scope.row.onlineStatus)">{{ onlineLabel(scope.row.onlineStatus) }}</span>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
@@ -133,7 +133,7 @@
<script>
import { treeArea } from '@/api/visitor-admin/area'
import { listDevice, getDevice, addDevice, updateDevice, changeDeviceStatus, logoutDevice, delDevice } from '@/api/visitor-admin/device'
-import { getResponseData, getPageRows, getPageTotal, normalizeBoolean, onlineText, toIdArray } from '@/utils/visitor-admin'
+import { getResponseData, getPageRows, getPageTotal, normalizeBoolean, onlineStatusClass, onlineText, toIdArray } from '@/utils/visitor-admin'
export default {
name: 'VisitorDevice',
@@ -244,6 +244,10 @@
onlineLabel(value) {
return onlineText(value)
},
+ // 目的:根据数据库在线状态枚举返回对应的标签样式。
+ onlineClass(value) {
+ return onlineStatusClass(value)
+ },
// 目的:按区域名称关键字过滤区域树节点。
filterAreaNode(value, data) {
return !value || (data.areaName || '').indexOf(value) !== -1
diff --git a/ruoyi-ui/src/views/visitor-admin/record/visitor.vue b/ruoyi-ui/src/views/visitor-admin/record/visitor.vue
index cd9c201..d56e72d 100644
--- a/ruoyi-ui/src/views/visitor-admin/record/visitor.vue
+++ b/ruoyi-ui/src/views/visitor-admin/record/visitor.vue
@@ -125,7 +125,7 @@
{ value: 'VISITING', label: '访问中', aliases: ['visiting'] },
{ value: 'SIGNED', label: '已签到', aliases: ['signed'] },
{ value: 'SIGNED_OUT', label: '已签离', aliases: ['signed_out'] },
- { value: 'OVERDUE_AUTO_SIGNED_OUT', label: '逾期自动签离', aliases: ['overdue_auto_signed_out'] },
+ { value: 'OVERDUE_AUTO_SIGNED_OUT', label: '超期自动签离', aliases: ['overdue_auto_signed_out'] },
{ value: 'EXPIRED', label: '已过期', aliases: ['expired'] },
{ value: 'CANCELLED', label: '已取消', aliases: ['cancelled', 'canceled', 'CANCELED'] }
],
diff --git a/ruoyi-ui/src/views/visitor-admin/system/config.vue b/ruoyi-ui/src/views/visitor-admin/system/config.vue
index aeb0039..b684e2e 100644
--- a/ruoyi-ui/src/views/visitor-admin/system/config.vue
+++ b/ruoyi-ui/src/views/visitor-admin/system/config.vue
@@ -11,7 +11,7 @@
type="button"
@click="activeTab = tab.value"
>
- <span class="nav-folder"></span>
+ <svg-icon class-name="config-nav-icon" :icon-class="tab.icon" />
<span>{{ tab.label }}</span>
</button>
</aside>
@@ -20,33 +20,52 @@
<section v-show="activeTab === 'register'">
<div class="config-section">
<h2>登记模式设置</h2>
- <label v-for="item in registerSwitches" :key="item.prop" class="config-switch-row">
- <span class="row-icon"></span>
- <span class="row-label">{{ item.label }}</span>
- <input v-model="form[item.prop]" type="checkbox">
- <span class="switch-control"></span>
+ <label class="config-switch-row">
+ <svg-icon class-name="row-icon" icon-class="config-onsite-register" />
+ <span class="row-label">现场登记</span>
+ <input v-model="form.onsiteRegistrationEnabled" type="checkbox">
+ <span class="switch-control" />
</label>
+ <label class="config-switch-row">
+ <svg-icon class-name="row-icon" icon-class="config-appointment-register" />
+ <span class="row-label">预约登记</span>
+ <input v-model="form.appointmentRegistrationEnabled" type="checkbox">
+ <span class="switch-control" />
+ </label>
+ <div v-if="form.appointmentRegistrationEnabled" class="appointment-time-row">
+ <svg-icon class-name="row-icon" icon-class="config-appointment-time" />
+ <span class="row-label">预约登记默认访问时间</span>
+ <span class="time-day">当天</span>
+ <el-time-picker
+ v-model="form.appointmentDefaultVisitTime"
+ class="appointment-time-input"
+ value-format="HH:mm"
+ format="HH:mm"
+ placeholder="请选择时间"
+ :clearable="false"
+ />
+ </div>
</div>
<div class="config-section">
<h2>接待人验证开关</h2>
<p>设置登记、签离时是否需要接待人确认。若关闭,访客认证后直接打印凭条/签离时自动回收门禁权限。</p>
<label v-for="item in receiverSwitches" :key="item.prop" class="config-switch-row">
- <span class="row-icon"></span>
+ <svg-icon class-name="row-icon" :icon-class="item.icon" />
<span class="row-label">{{ item.label }}</span>
<input v-model="form[item.prop]" type="checkbox">
- <span class="switch-control"></span>
+ <span class="switch-control" />
</label>
</div>
<div class="config-section">
<h2>人脸验证方式</h2>
- <p>人像对比即身份证和人脸对比确保为同一人(手动录入身份证除外),全方位人脸采集则360°采集人脸照片。</p>
+ <p>人像对比即身份证和人脸对比确保为同一人(手动录入身份证除外),人脸采集则采集访客人脸照片。</p>
<label v-for="item in faceSwitches" :key="item.prop" class="config-switch-row">
- <span class="row-icon"></span>
+ <svg-icon class-name="row-icon" :icon-class="item.icon" />
<span class="row-label">{{ item.label }}</span>
<input v-model="form[item.prop]" type="checkbox">
- <span class="switch-control"></span>
+ <span class="switch-control" />
</label>
</div>
@@ -54,10 +73,10 @@
<h2>身份证手动输入</h2>
<p>关闭后仅可刷身份证登记/签离。强制人脸比对时,开启则支持手动输入身份证并做人脸采集。</p>
<label class="config-switch-row">
- <span class="row-icon"></span>
+ <svg-icon class-name="row-icon" icon-class="config-id-card" />
<span class="row-label">允许手动输入身份证</span>
<input v-model="form.manualIdCardEnabled" type="checkbox">
- <span class="switch-control"></span>
+ <span class="switch-control" />
</label>
</div>
</section>
@@ -68,15 +87,14 @@
<div class="config-section">
<h2>凭条样式配置</h2>
<p>勾选需要在凭条上打印的字段,支持拖拽排序(至少勾选1个字段)</p>
- <draggable v-model="printFieldOptions" class="print-field-list" handle=".print-field-drag" ghost-class="print-field-ghost" :delay="120" @end="syncPrintFieldsOrder">
+ <draggable v-model="printFieldOptions" class="print-field-list" ghost-class="print-field-ghost" chosen-class="print-field-chosen" :delay="120" :animation="160" @end="syncPrintFieldsOrder">
<label v-for="item in printFieldOptions" :key="item.fieldCode" class="print-field-item">
- <span class="print-field-drag">↕</span>
<input v-model="form.printFields" type="checkbox" :value="item.fieldCode">
<span>{{ item.fieldName }}</span>
</label>
</draggable>
<button class="sort-hint" type="button">
- <span>↕</span>
+ <i class="el-icon-s-opportunity sort-hint-icon" aria-hidden="true" />
<span>长按选项拖拽排序</span>
</button>
</div>
@@ -85,17 +103,21 @@
<aside class="print-preview">
<div class="preview-title">
<span>效果预览</span>
- <button type="button" title="刷新预览" @click="refreshPreview">↻</button>
+ <button type="button" title="刷新预览" aria-label="刷新预览" @click="refreshPreview">
+ <i class="el-icon-refresh" aria-hidden="true" />
+ </button>
</div>
- <section class="ticket-preview" aria-label="登记凭证预览">
- <h3>登记凭证</h3>
- <div class="ticket-rows">
- <div v-for="row in ticketRows" :key="row.label" class="ticket-row">
- <strong>{{ row.label }}</strong>
- <span>{{ row.value }}</span>
+ <div class="preview-stage">
+ <section class="ticket-preview" aria-label="登记凭证预览">
+ <h3>登记凭证</h3>
+ <div class="ticket-rows">
+ <div v-for="row in ticketRows" :key="row.label" class="ticket-row">
+ <strong>{{ row.label }}</strong>
+ <span>{{ row.value }}</span>
+ </div>
</div>
- </div>
- </section>
+ </section>
+ </div>
</aside>
</div>
</section>
@@ -109,13 +131,14 @@
<span>系统名称</span>
<input v-model="form.systemName" type="text" placeholder="请输入系统名称">
</label>
- <label class="input-field">
+ <div class="input-field">
<span>Logo</span>
- <div class="logo-upload">
+ <label class="logo-upload">
<input v-hasPermi="['visitor:config:edit']" type="file" accept="image/*" :disabled="uploadingLogo" @change="handleLogoChange">
- <span>{{ uploadingLogo ? '上传中' : logoFileName }}</span>
- </div>
- </label>
+ <span class="logo-upload-button">选择文件</span>
+ <span class="logo-file-name">{{ uploadingLogo ? '上传中' : logoFileName }}</span>
+ </label>
+ </div>
</div>
</div>
@@ -137,10 +160,10 @@
<h2>入口配置</h2>
<p>关闭后入口不显示,但是仍可以点击该区域进入。</p>
<label class="config-switch-row">
- <span class="row-icon"></span>
+ <svg-icon class-name="row-icon" icon-class="config-device" />
<span class="row-label">访客机管理侧入口</span>
<input v-model="form.visitorMachineEntryEnabled" type="checkbox">
- <span class="switch-control"></span>
+ <span class="switch-control" />
</label>
</div>
</div>
@@ -163,6 +186,7 @@
const defaultForm = {
onsiteRegistrationEnabled: true,
appointmentRegistrationEnabled: true,
+ appointmentDefaultVisitTime: '',
registerReceiverConfirmEnabled: false,
signReceiverConfirmEnabled: false,
faceCompareEnabled: false,
@@ -188,21 +212,17 @@
logoFileName: '未选择任何文件',
form: { ...defaultForm },
tabs: [
- { value: 'register', label: '登记配置' },
- { value: 'print', label: '打印凭条配置' },
- { value: 'system', label: '系统信息' }
- ],
- registerSwitches: [
- { prop: 'onsiteRegistrationEnabled', label: '现场登记' },
- { prop: 'appointmentRegistrationEnabled', label: '预约登记' }
+ { value: 'register', label: '登记配置', icon: 'config-register' },
+ { value: 'print', label: '打印凭条配置', icon: 'config-print' },
+ { value: 'system', label: '系统信息', icon: 'config-system' }
],
receiverSwitches: [
- { prop: 'registerReceiverConfirmEnabled', label: '登记需接待人确认' },
- { prop: 'signReceiverConfirmEnabled', label: '签离需接待人确认' }
+ { prop: 'registerReceiverConfirmEnabled', label: '登记需接待人确认', icon: 'config-receiver-confirm' },
+ { prop: 'signReceiverConfirmEnabled', label: '签离需接待人确认', icon: 'config-sign-confirm' }
],
faceSwitches: [
- { prop: 'faceCompareEnabled', label: '人像对比' },
- { prop: 'faceCaptureEnabled', label: '全方位人脸采集' }
+ { prop: 'faceCompareEnabled', label: '人像对比', icon: 'config-face-compare' },
+ { prop: 'faceCaptureEnabled', label: '人脸采集', icon: 'config-face-capture' }
],
printFieldOptions: [
{ fieldCode: 'visitorName', fieldName: '访客姓名' },
diff --git a/ruoyi-ui/src/views/visitor-admin/system/protocol.vue b/ruoyi-ui/src/views/visitor-admin/system/protocol.vue
index 6518781..6b17a1e 100644
--- a/ruoyi-ui/src/views/visitor-admin/system/protocol.vue
+++ b/ruoyi-ui/src/views/visitor-admin/system/protocol.vue
@@ -10,25 +10,30 @@
<form class="protocol-form" @submit.prevent="submitForm">
<section class="editor-shell" :class="{ fullscreen }">
<div v-if="canEditProtocol" class="editor-toolbar" role="toolbar" aria-label="富文本工具栏">
- <button type="button" title="加粗" @click="exec('bold')"><strong>B</strong></button>
- <button type="button" title="斜体" @click="exec('italic')"><em>I</em></button>
- <button type="button" title="文字颜色" @click="exec('foreColor', '#111827')"><span class="font-color">A</span></button>
- <button type="button" title="标题字号" @click="exec('fontSize', '4')"><span class="font-size">T<small>T</small></span></button>
- <button type="button" title="无序列表" @click="exec('insertUnorderedList')"><span class="list-icon unordered"></span></button>
- <button type="button" title="有序列表" @click="exec('insertOrderedList')"><span class="list-icon ordered"></span></button>
- <button type="button" title="左对齐" @click="exec('justifyLeft')"><span class="align-icon left"></span></button>
- <button type="button" title="居中" @click="exec('justifyCenter')"><span class="align-icon center"></span></button>
- <button type="button" title="右对齐" @click="exec('justifyRight')"><span class="align-icon right"></span></button>
- <button type="button" title="撤销" @click="exec('undo')"><span class="undo-icon">↶</span></button>
- <button type="button" title="重做" @click="exec('redo')"><span class="redo-icon">↷</span></button>
+ <button type="button" title="加粗" @mousedown.prevent="exec('bold')"><strong>B</strong></button>
+ <button type="button" title="斜体" @mousedown.prevent="exec('italic')"><em>I</em></button>
+ <button type="button" title="文字颜色" @mousedown.prevent="exec('foreColor', '#111827')"><span class="font-color">A</span></button>
+ <button type="button" title="标题字号" @mousedown.prevent="exec('fontSize', '4')"><span class="font-size">T<small>T</small></span></button>
+ <button type="button" title="无序列表" @mousedown.prevent="exec('insertUnorderedList')"><span class="list-icon unordered"></span></button>
+ <button type="button" title="有序列表" @mousedown.prevent="exec('insertOrderedList')"><span class="list-icon ordered"></span></button>
+ <button type="button" title="左对齐" @mousedown.prevent="exec('justifyLeft')"><span class="align-icon left"></span></button>
+ <button type="button" title="居中" @mousedown.prevent="exec('justifyCenter')"><span class="align-icon center"></span></button>
+ <button type="button" title="右对齐" @mousedown.prevent="exec('justifyRight')"><span class="align-icon right"></span></button>
+ <button type="button" title="撤销" @mousedown.prevent="exec('undo')"><span class="undo-icon">↶</span></button>
+ <button type="button" title="重做" @mousedown.prevent="exec('redo')"><span class="redo-icon">↷</span></button>
<button type="button" title="全屏编辑" @click="toggleFullscreen"><span class="fullscreen-icon"></span></button>
</div>
<div
ref="editor"
class="protocol-editor"
+ :class="{ 'is-empty': editorEmpty }"
:contenteditable="canEditProtocol"
data-placeholder="请输入"
- @input="syncEditor"
+ @focus="handleEditorFocus"
+ @input="handleEditorInput"
+ @mouseup="saveSelection"
+ @keyup="saveSelection"
+ @keydown.enter="handleEditorEnter"
@paste="handlePaste"
></div>
</section>
@@ -55,6 +60,8 @@
return {
saving: false,
fullscreen: false,
+ editorEmpty: true,
+ savedRange: null,
form: {
protocolType,
protocolName,
@@ -97,6 +104,9 @@
this.$nextTick(() => {
if (this.$refs.editor) {
this.$refs.editor.innerHTML = this.form.protocolContent
+ this.savedRange = null
+ this.ensureEditorBlock(false)
+ this.updateEditorEmpty()
}
})
})
@@ -107,8 +117,94 @@
this.denyMiddlePlatformWrite()
return
}
- this.$refs.editor && this.$refs.editor.focus()
+ const editor = this.$refs.editor
+ if (!editor) {
+ return
+ }
+ editor.focus()
+ this.restoreSelection()
+ if (command === 'justifyLeft' || command === 'justifyCenter' || command === 'justifyRight') {
+ document.execCommand('formatBlock', false, 'div')
+ }
+ if (command !== 'undo' && command !== 'redo') {
+ document.execCommand('styleWithCSS', false, true)
+ }
document.execCommand(command, false, value || null)
+ this.saveSelection()
+ this.syncEditor()
+ },
+ // 目的:聚焦编辑器时确保块级结构存在,并记录当前光标位置供工具栏命令复用。
+ handleEditorFocus() {
+ this.ensureEditorBlock()
+ this.saveSelection()
+ },
+ // 目的:输入后同步表单内容,并保存最新选区,避免点击工具栏时丢失格式作用位置。
+ handleEditorInput() {
+ this.syncEditor()
+ this.saveSelection()
+ },
+ // 目的:仅保存编辑器内部选区,防止页面其他区域的选区被富文本命令误用。
+ saveSelection() {
+ const editor = this.$refs.editor
+ const selection = window.getSelection()
+ if (!editor || !selection || !selection.rangeCount) {
+ return
+ }
+ const range = selection.getRangeAt(0)
+ const container = range.commonAncestorContainer
+ if (container === editor || editor.contains(container)) {
+ this.savedRange = range.cloneRange()
+ }
+ },
+ // 目的:执行工具栏命令前恢复编辑器选区,使居中等块级格式能够应用到选中段落。
+ restoreSelection() {
+ if (!this.savedRange) {
+ return
+ }
+ const selection = window.getSelection()
+ selection.removeAllRanges()
+ selection.addRange(this.savedRange)
+ },
+ // 目的:确保编辑内容由块级行容器承载,使换行和对齐样式能够进入保存的 HTML。
+ ensureEditorBlock(moveCaret = true) {
+ const editor = this.$refs.editor
+ if (!editor || editor.querySelector('div, p, ul, ol, blockquote, table')) {
+ return
+ }
+ const block = document.createElement('div')
+ const rootTextAlign = editor.style.textAlign
+ while (editor.firstChild) {
+ block.appendChild(editor.firstChild)
+ }
+ if (!block.childNodes.length) {
+ block.appendChild(document.createElement('br'))
+ }
+ if (rootTextAlign) {
+ block.style.textAlign = rootTextAlign
+ editor.style.textAlign = ''
+ }
+ editor.appendChild(block)
+ const selection = moveCaret ? window.getSelection() : null
+ if (selection) {
+ const range = document.createRange()
+ range.selectNodeContents(block)
+ range.collapse(false)
+ selection.removeAllRanges()
+ selection.addRange(range)
+ }
+ this.updateEditorEmpty()
+ },
+ // 目的:显式插入新的块级段落,确保回车换行能够作为 HTML 结构持久化到数据库。
+ handleEditorEnter(event) {
+ if (!this.canEditProtocol) {
+ event.preventDefault()
+ this.denyMiddlePlatformWrite()
+ return
+ }
+ event.preventDefault()
+ this.ensureEditorBlock()
+ document.execCommand('defaultParagraphSeparator', false, 'div')
+ document.execCommand('insertParagraph', false, null)
this.syncEditor()
},
// 目的:切换编辑器全屏状态,并在切换后保持编辑焦点。
@@ -139,7 +235,45 @@
},
// 目的:把编辑器 DOM 内容同步回表单,并再次清洗 HTML。
syncEditor() {
- this.form.protocolContent = this.$refs.editor ? this.sanitizeHtml(this.$refs.editor.innerHTML) : ''
+ this.form.protocolContent = this.serializeEditorContent()
+ this.updateEditorEmpty()
+ },
+ // 目的:将编辑器顶层内容统一为块级段落,并用换行符分隔后提交数据库。
+ serializeEditorContent() {
+ const editor = this.$refs.editor
+ if (!editor) {
+ return ''
+ }
+ const container = document.createElement('div')
+ const blockTags = ['DIV', 'P', 'UL', 'OL', 'BLOCKQUOTE', 'PRE', 'TABLE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6']
+ let currentBlock = null
+ Array.prototype.slice.call(editor.childNodes).forEach(node => {
+ const isBlock = node.nodeType === 1 && blockTags.indexOf(node.nodeName) !== -1
+ if (isBlock) {
+ currentBlock = null
+ container.appendChild(node.cloneNode(true))
+ return
+ }
+ if (!currentBlock) {
+ currentBlock = document.createElement('div')
+ container.appendChild(currentBlock)
+ }
+ currentBlock.appendChild(node.cloneNode(true))
+ })
+ const html = Array.prototype.slice.call(container.childNodes)
+ .map(node => node.outerHTML)
+ .join('\n')
+ return this.sanitizeHtml(html)
+ },
+ // 目的:根据编辑器真实文本和媒体内容维护占位提示显示状态。
+ updateEditorEmpty() {
+ const editor = this.$refs.editor
+ if (!editor) {
+ this.editorEmpty = true
+ return
+ }
+ const text = String(editor.textContent || '').replace(/\u200B/g, '').trim()
+ this.editorEmpty = !text && !editor.querySelector('img, video, audio, table, hr')
},
// 目的:校验并保存协议配置;中台只读模式下阻止提交。
submitForm() {
@@ -153,6 +287,7 @@
this.$modal.msgError('请输入入园承诺内容')
return
}
+ const submittedContent = this.form.protocolContent
this.saving = true
saveProtocolConfig({
...this.form,
@@ -161,10 +296,14 @@
enabled: true
}).then(res => {
const data = getResponseData(res) || {}
- this.form.protocolContent = this.sanitizeHtml(data.protocolContent || this.form.protocolContent)
- if (this.$refs.editor) {
- this.$refs.editor.innerHTML = this.form.protocolContent
+ this.form = {
+ ...this.form,
+ ...data,
+ protocolType,
+ protocolName,
+ protocolContent: submittedContent
}
+ this.updateEditorEmpty()
this.$modal.msgSuccess('保存成功')
}).finally(() => {
this.saving = false
@@ -186,6 +325,13 @@
}
})
})
+ template.content.querySelectorAll('[align]').forEach(node => {
+ const align = String(node.getAttribute('align') || '').toLowerCase()
+ if (['left', 'center', 'right', 'justify'].indexOf(align) !== -1) {
+ node.style.textAlign = align
+ }
+ node.removeAttribute('align')
+ })
return template.innerHTML
},
// 目的:将纯文本转义为安全 HTML,供纯文本粘贴时保留换行展示。
diff --git a/ruoyi-ui/src/views/visitor-admin/system/version.vue b/ruoyi-ui/src/views/visitor-admin/system/version.vue
index 8c42ae2..4b20bb6 100644
--- a/ruoyi-ui/src/views/visitor-admin/system/version.vue
+++ b/ruoyi-ui/src/views/visitor-admin/system/version.vue
@@ -13,20 +13,28 @@
<th>版本号</th>
<th>更新内容</th>
<th>创建时间</th>
- <th v-if="!isMiddlePlatformReadonly && $auth.hasPermi('visitor:version:remove')">操作</th>
+ <th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in versionList" :key="row.updateId || index">
<td>{{ index + 1 }}</td>
- <td>v{{ row.versionNo }}</td>
+ <td>
+ {{ formatVersionNo(row.versionNo) }}
+ <el-tag v-if="row.latest" size="mini" type="success">最新</el-tag>
+ </td>
<td class="version-content-cell">
<span class="version-content-text">{{ row.updateContent || '-' }}</span>
</td>
<td>{{ parseTime(row.createTime) }}</td>
- <td v-if="!isMiddlePlatformReadonly && $auth.hasPermi('visitor:version:remove')">
+ <td>
<div class="table-actions">
- <button v-hasPermi="['visitor:version:remove']" type="button" @click="handleDelete(row)">删除</button>
+ <button type="button" @click="handleView(row)">查看</button>
+ <button
+ v-if="!row.latest && !isMiddlePlatformReadonly && $auth.hasPermi('visitor:version:remove')"
+ type="button"
+ @click="handleDelete(row)"
+ >删除</button>
</div>
</td>
</tr>
@@ -36,36 +44,70 @@
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</section>
- <el-dialog title="新建" :visible.sync="open" width="482px" append-to-body>
+ <el-dialog title="版本详情" :visible.sync="detailOpen" width="560px" append-to-body>
+ <el-form label-width="100px">
+ <el-form-item label="版本号">
+ <span>{{ formatVersionNo(detail.versionNo) }}</span>
+ <el-tag v-if="detail.latest" size="mini" type="success">最新</el-tag>
+ </el-form-item>
+ <el-form-item label="更新内容">
+ <div style="white-space: pre-wrap; word-break: break-all;">{{ detail.updateContent || '-' }}</div>
+ </el-form-item>
+ <el-form-item label="安装包">{{ detail.packageName || detail.packageUrl || '-' }}</el-form-item>
+ <el-form-item label="强制更新">{{ detail.forceUpdate ? '是' : '否' }}</el-form-item>
+ <el-form-item label="创建时间">{{ parseTime(detail.createTime) || '-' }}</el-form-item>
+ </el-form>
+ <div slot="footer" class="dialog-footer">
+ <el-button @click="detailOpen = false">关闭</el-button>
+ </div>
+ </el-dialog>
+
+ <el-dialog title="新建" :visible.sync="open" width="482px" append-to-body custom-class="version-create-dialog">
<form class="version-form" @submit.prevent="submitForm">
<label class="version-row required">
<span>版本号:</span>
<div class="version-input-line">
<span>v</span>
- <input v-model="form.versionNo" class="visitor-native-input" type="text" placeholder="请输入">
+ <input
+ v-model="form.versionNo"
+ class="visitor-native-input"
+ type="text"
+ inputmode="decimal"
+ pattern="[0-9]+(\.[0-9]+)*"
+ placeholder="请输入"
+ @input="handleVersionNoInput"
+ >
</div>
</label>
<label class="version-row">
<span>更新内容</span>
- <textarea v-model="form.updateContent" class="visitor-native-textarea" placeholder="请输入"></textarea>
+ <textarea v-model="form.updateContent" class="visitor-native-textarea" placeholder="请输入" />
</label>
<div class="version-row required">
<span>安装包:</span>
- <div>
- <button class="upload-drop" :class="{ 'has-file': packageFileName }" type="button" @click="$refs.packageFile.click()">
+ <div class="version-upload-field">
+ <button
+ class="upload-drop"
+ :class="{ 'has-file': packageFileName, 'drag-over': packageDragActive }"
+ type="button"
+ @click="$refs.packageFile.click()"
+ @dragover.prevent="handlePackageDragOver"
+ @dragleave="handlePackageDragLeave"
+ @drop.prevent="handlePackageDrop"
+ >
<input ref="packageFile" type="file" hidden @change="handlePackageChange">
<span><strong>{{ packageFileName ? packageFileName : '点击上传' }}</strong><i v-if="!packageFileName"> / </i>{{ packageFileName ? '' : '拖拽到此区域' }}</span>
</button>
- <p class="upload-tip">请上传安装包文件,大小在60M以内</p>
+ <p class="upload-tip">请上传安装包文件,大小须小于200M</p>
</div>
</div>
<label class="version-row switch-line">
<span>强制更新</span>
<input v-model="form.forceUpdate" type="checkbox">
- <span class="switch-control"></span>
+ <span class="switch-control" />
</label>
<footer class="visitor-form-actions">
@@ -78,8 +120,10 @@
</template>
<script>
-import { listVersion, addVersion, delVersion } from '@/api/visitor-admin/version'
-import { getPageRows, getPageTotal } from '@/utils/visitor-admin'
+import { listVersion, getVersion, addVersion, delVersion, uploadVersionPackage } from '@/api/visitor-admin/version'
+import { getPageRows, getPageTotal, getResponseData } from '@/utils/visitor-admin'
+
+const MAX_PACKAGE_SIZE = 200 * 1024 * 1024
export default {
name: 'VisitorVersionUpdate',
@@ -89,7 +133,11 @@
loading: false,
total: 0,
open: false,
+ detailOpen: false,
+ detail: {},
+ packageFile: null,
packageFileName: '',
+ packageDragActive: false,
versionList: [],
queryParams: {
pageNum: 1,
@@ -114,6 +162,11 @@
this.loading = false
})
},
+ // 目的:兼容历史版本号是否包含 v 前缀,统一按 v+版本号展示。
+ formatVersionNo(versionNo) {
+ const value = String(versionNo || '').replace(/^[vV]/, '')
+ return value ? `v${value}` : '-'
+ },
// 目的:打开新增版本弹窗;中台只读模式下禁止本地新增。
handleAdd() {
if (this.isMiddlePlatformReadonly) {
@@ -121,16 +174,59 @@
return
}
this.form = { forceUpdate: false, versionNo: '', updateContent: '', packageName: '', packageUrl: '' }
+ this.packageFile = null
this.packageFileName = ''
+ this.packageDragActive = false
+ if (this.$refs.packageFile) {
+ this.$refs.packageFile.value = ''
+ }
this.open = true
},
- // 目的:记录选择的安装包文件名,并回填版本包名称和地址字段。
+ // 目的:版本号只保留数字和小数点,支持多段版本号输入。
+ handleVersionNoInput(event) {
+ const versionNo = event.target.value.replace(/[^\d.]/g, '').replace(/\.{2,}/g, '.')
+ event.target.value = versionNo
+ this.form.versionNo = versionNo
+ },
+ // 目的:通过文件选择框读取安装包,并复用统一校验逻辑。
handlePackageChange(event) {
const file = event.target.files && event.target.files[0]
+ this.setPackageFile(file, event.target)
+ },
+ // 目的:拖拽文件经过上传区域时阻止浏览器默认行为并展示激活状态。
+ handlePackageDragOver() {
+ this.packageDragActive = true
+ },
+ // 目的:文件离开整个上传区域后恢复默认样式,避免子元素切换导致闪烁。
+ handlePackageDragLeave(event) {
+ if (!event.currentTarget.contains(event.relatedTarget)) {
+ this.packageDragActive = false
+ }
+ },
+ // 目的:接收拖放的首个文件,并复用文件选择框的校验和回填逻辑。
+ handlePackageDrop(event) {
+ this.packageDragActive = false
+ const file = event.dataTransfer.files && event.dataTransfer.files[0]
+ this.setPackageFile(file, this.$refs.packageFile)
+ },
+ // 目的:统一处理点击选择与拖拽选择的安装包,避免重复校验逻辑。
+ setPackageFile(file, input) {
+ if (file && file.size >= MAX_PACKAGE_SIZE) {
+ this.$modal.msgError('操作失败:安装包大小必须小于200M')
+ if (input) {
+ input.value = ''
+ }
+ this.packageFile = null
+ this.packageFileName = ''
+ this.form.packageName = ''
+ this.form.packageUrl = ''
+ return
+ }
+ this.packageFile = file || null
this.packageFileName = file ? file.name : ''
if (file) {
this.form.packageName = file.name
- this.form.packageUrl = file.name
+ this.form.packageUrl = ''
}
},
// 目的:校验并提交新增版本;中台只读模式下禁止提交。
@@ -139,14 +235,35 @@
this.denyMiddlePlatformWrite()
return
}
- if (!this.form.versionNo || !this.form.packageUrl) {
+ if (!/^\d+(\.\d+)*$/.test(this.form.versionNo || '') || !this.packageFile) {
this.$modal.msgError('请填写版本号并上传安装包')
return
}
- addVersion(this.form).then(() => {
+ if (this.packageFile.size >= MAX_PACKAGE_SIZE) {
+ this.$modal.msgError('操作失败:安装包大小必须小于200M')
+ return
+ }
+ const formData = new FormData()
+ formData.append('file', this.packageFile)
+ uploadVersionPackage(formData).then(res => {
+ const data = getResponseData(res) || {}
+ this.form.packageUrl = data.fileName || data.url || ''
+ this.form.packageName = data.originalFilename || data.newFileName || this.packageFile.name
+ if (!this.form.packageUrl) {
+ throw new Error('安装包上传失败,未返回文件地址')
+ }
+ return addVersion(this.form)
+ }).then(() => {
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
+ })
+ },
+ // 目的:查看任意版本的完整更新信息。
+ handleView(row) {
+ getVersion(row.updateId).then(res => {
+ this.detail = getResponseData(res) || {}
+ this.detailOpen = true
})
},
// 目的:删除版本更新记录;中台只读模式下禁止本地删除。
@@ -155,6 +272,10 @@
this.denyMiddlePlatformWrite()
return
}
+ if (row.latest) {
+ this.$modal.msgWarning('最新版本不能删除')
+ return
+ }
this.$modal.confirm('是否确认删除该版本记录?').then(() => delVersion([row.updateId])).then(() => {
this.$modal.msgSuccess('删除成功')
this.getList()
diff --git a/visitor-admin-ui/css/version-update.css b/visitor-admin-ui/css/version-update.css
index 294f8d4..267bda6 100644
--- a/visitor-admin-ui/css/version-update.css
+++ b/visitor-admin-ui/css/version-update.css
@@ -141,7 +141,7 @@
.version-modal-header {
height: 52px;
justify-content: flex-start;
- padding-left: 22px;
+ padding-left: 20px;
}
.version-modal-header h3 {
@@ -165,7 +165,10 @@
.version-row > span:first-child {
padding-top: 8px;
- text-align: right;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ text-align: left;
}
.version-row.required > span:first-child::before {
@@ -178,24 +181,31 @@
display: grid;
grid-template-columns: 18px 1fr;
align-items: center;
+ width: 242px;
+ height: 24px;
+ border: 1px solid #cbd2dc;
+ box-sizing: border-box;
}
.version-prefix {
+ padding-left: 7px;
color: #24364f;
}
.version-row input[type="text"] {
- height: 24px;
+ height: 22px;
min-width: 0;
- padding: 0 8px;
- border: 1px solid #cbd2dc;
+ padding: 0 7px 0 0;
+ border: 0;
color: #1f2d3d;
+ font: inherit;
outline: 0;
}
.version-row textarea {
- width: 100%;
+ width: 242px;
height: 68px;
+ box-sizing: border-box;
resize: none;
padding: 8px;
border: 1px solid #cbd2dc;
@@ -205,10 +215,14 @@
outline: 0;
}
-.version-row input[type="text"]:focus,
+.version-input-line:focus-within,
.version-row textarea:focus {
border-color: #2f80ed;
box-shadow: 0 0 0 2px rgba(47, 128, 237, 0.08);
+}
+
+.upload-field {
+ width: 242px;
}
.upload-drop {
@@ -249,6 +263,7 @@
margin: 10px 0 0;
color: #8a9ab0;
font-size: 11px;
+ line-height: 16px;
text-align: center;
}
@@ -286,7 +301,7 @@
}
.switch-line input:checked + .switch-control {
- background: #21c96b;
+ background: #19be6b;
}
.switch-line input:checked + .switch-control::after {
diff --git a/visitor-admin-ui/js/version-update.js b/visitor-admin-ui/js/version-update.js
index d6a1d95..6c8622f 100644
--- a/visitor-admin-ui/js/version-update.js
+++ b/visitor-admin-ui/js/version-update.js
@@ -3,7 +3,7 @@
apiBaseUrl: "http://localhost:9098"
}, window.VISITOR_LOGIN_CONFIG || {});
- var MAX_PACKAGE_SIZE = 60 * 1024 * 1024;
+ var MAX_PACKAGE_SIZE = 200 * 1024 * 1024;
var state = {
pageNum: 1,
pageSize: 20,
@@ -69,6 +69,7 @@
function bindPageActions() {
var uploadDrop = document.getElementById("uploadDrop");
var packageFile = document.getElementById("packageFile");
+ var versionNoInput = document.getElementById("versionNo");
document.getElementById("addVersionButton").addEventListener("click", openAddModal);
document.getElementById("versionForm").addEventListener("submit", function (event) {
@@ -78,6 +79,10 @@
return;
}
saveVersion();
+ });
+
+ versionNoInput.addEventListener("input", function () {
+ versionNoInput.value = versionNoInput.value.replace(/[^\d.]/g, "").replace(/\.{2,}/g, ".");
});
document.querySelectorAll("[data-close='version']").forEach(function (button) {
@@ -231,11 +236,15 @@
function saveVersion() {
var saveButton = document.getElementById("saveVersionButton");
- var versionNo = normalizeVersionNo(document.getElementById("versionNo").value);
+ var versionNumber = document.getElementById("versionNo").value.trim();
var updateContent = document.getElementById("updateContent").value.trim();
- if (!versionNo) {
+ if (!versionNumber) {
AppMessage.error("操作失败:请输入版本号");
+ return;
+ }
+ if (!/^\d+(\.\d+)*$/.test(versionNumber)) {
+ AppMessage.error("操作失败:版本号格式不正确,请输入数字或使用小数点分隔");
return;
}
if (!updateContent) {
@@ -246,13 +255,17 @@
AppMessage.error("操作失败:请上传安装包");
return;
}
+ if (isPackageTooLarge(state.packageFile)) {
+ AppMessage.error("操作失败:安装包大小必须小于200M");
+ return;
+ }
saveButton.disabled = true;
uploadPackage().then(function (fileInfo) {
return apiRequest("/system/version/update/add", {
method: "POST",
body: JSON.stringify({
- versionNo: versionNo,
+ versionNo: normalizeVersionNo(versionNumber),
updateContent: updateContent,
packageUrl: fileInfo.url || fileInfo.fileName || "",
packageName: fileInfo.originalFilename || fileInfo.newFileName || state.packageFile.name,
@@ -271,9 +284,12 @@
}
function uploadPackage() {
+ if (isPackageTooLarge(state.packageFile)) {
+ return Promise.reject(new Error("安装包大小必须小于200M"));
+ }
var formData = new FormData();
formData.append("file", state.packageFile);
- return apiRequest("/common/upload", {
+ return apiRequest("/system/version/update/upload", {
method: "POST",
body: formData,
skipJson: true
@@ -286,20 +302,24 @@
updatePackageTip();
return;
}
- if (file.size > MAX_PACKAGE_SIZE) {
+ if (isPackageTooLarge(file)) {
state.packageFile = null;
document.getElementById("packageFile").value = "";
updatePackageTip();
- AppMessage.error("操作失败:安装包大小不能超过60M");
+ AppMessage.error("操作失败:安装包大小必须小于200M");
return;
}
state.packageFile = file;
updatePackageTip(file.name);
}
+ function isPackageTooLarge(file) {
+ return !!file && file.size >= MAX_PACKAGE_SIZE;
+ }
+
function updatePackageTip(fileName) {
var uploadDrop = document.getElementById("uploadDrop");
- var text = fileName || "请上传安装包文件,大小在60M以内";
+ var text = fileName || "请上传安装包文件,大小须小于200M";
document.getElementById("packageTip").textContent = text;
uploadDrop.classList.toggle("has-file", !!fileName);
uploadDrop.querySelector("span").innerHTML = fileName
diff --git a/visitor-admin-ui/pages/version-update.html b/visitor-admin-ui/pages/version-update.html
index f9e3c71..ec635cb 100644
--- a/visitor-admin-ui/pages/version-update.html
+++ b/visitor-admin-ui/pages/version-update.html
@@ -145,7 +145,7 @@
<span>版本号:</span>
<div class="version-input-line">
<span class="version-prefix">v</span>
- <input id="versionNo" type="text" placeholder="请输入">
+ <input id="versionNo" type="text" inputmode="decimal" pattern="[0-9]+(\.[0-9]+)*" autocomplete="off" placeholder="请输入">
</div>
</label>
@@ -156,12 +156,12 @@
<div class="version-row required">
<span>安装包:</span>
- <div>
+ <div class="upload-field">
<button id="uploadDrop" class="upload-drop" type="button">
<input id="packageFile" type="file" hidden>
<span><strong>点击上传</strong><i>/</i>拖拽到此区域</span>
</button>
- <p id="packageTip" class="upload-tip">请上传安装包文件,大小在60M以内</p>
+ <p id="packageTip" class="upload-tip">请上传安装包文件,大小须小于200M</p>
</div>
</div>
--
Gitblit v1.7.1