| | |
| | | `device_id` varchar(100) NOT NULL COMMENT '设备ID', |
| | | `enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', |
| | | `online_status` varchar(32) NOT NULL DEFAULT 'UNACTIVATED' COMMENT '在线状态:UNACTIVATED未激活 ONLINE在线 OFFLINE离线', |
| | | `fingerprint` varchar(255) DEFAULT NULL COMMENT '百度SDK指纹', |
| | | `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标识,0正常 1删除', |
| | | `create_by` varchar(64) DEFAULT '' COMMENT '创建者', |
| | | `create_time` datetime DEFAULT NULL COMMENT '创建时间', |
| | |
| | | @RequestMapping("/system/visitor/record") |
| | | public class SysVisitorRecordController extends BaseController |
| | | { |
| | | private static final int EXPORT_PAGE_SIZE = 10000; |
| | | |
| | | @Autowired |
| | | private ISysVisitorRecordService visitorRecordService; |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 导出访客登记记录 |
| | | */ |
| | | @Log(title = "访客登记记录", businessType = BusinessType.EXPORT) |
| | | @PreAuthorize("@ss.hasPermi('visitor:record:export')") |
| | | @PostMapping("/export") |
| | | public AjaxResult export(@RequestBody(required = false) SysVisitorRecordPageRequest request) |
| | | { |
| | | SysVisitorRecordPageRequest query = request == null ? new SysVisitorRecordPageRequest() : request; |
| | | query.setPageNum(1); |
| | | query.setPageSize(EXPORT_PAGE_SIZE); |
| | | return success(visitorRecordService.pageList(query).getRecords()); |
| | | } |
| | | |
| | | /** |
| | | * 查看访客登记详情 |
| | | */ |
| | | @Log(title = "访客登记记录", businessType = BusinessType.SELECT) |
| | |
| | | import io.swagger.annotations.ApiOperation; |
| | | import io.swagger.annotations.ApiResponse; |
| | | import io.swagger.annotations.ApiResponses; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellStyle; |
| | | import org.apache.poi.ss.usermodel.Font; |
| | | import org.apache.poi.ss.usermodel.HorizontalAlignment; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.xssf.streaming.SXSSFWorkbook; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.net.URLEncoder; |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 管理侧访客记录接口 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 导出访客记录 |
| | | */ |
| | | @ApiOperation( |
| | | value = "导出访客记录", |
| | | notes = "按分页查询接口的筛选条件查询全部访客记录并导出 Excel,不进行分页" |
| | | ) |
| | | @PostMapping("/export") |
| | | public void export(HttpServletResponse response, |
| | | @RequestBody(required = false) SysVisitorRecordPageRequest request) throws IOException |
| | | { |
| | | List<SysVisitorRecordVO> records = visitorRecordService.list(request); |
| | | response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); |
| | | response.setCharacterEncoding("utf-8"); |
| | | String fileName = URLEncoder.encode("访客记录", "UTF-8").replace("+", "%20"); |
| | | response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); |
| | | |
| | | SXSSFWorkbook workbook = new SXSSFWorkbook(500); |
| | | try |
| | | { |
| | | writeVisitorRecordSheet(workbook, records); |
| | | workbook.write(response.getOutputStream()); |
| | | response.flushBuffer(); |
| | | } |
| | | finally |
| | | { |
| | | workbook.dispose(); |
| | | workbook.close(); |
| | | } |
| | | } |
| | | |
| | | private void writeVisitorRecordSheet(SXSSFWorkbook workbook, List<SysVisitorRecordVO> records) |
| | | { |
| | | String[] headers = { |
| | | "登记编号", "访客姓名", "证件号码", "登记类型", "被访人", "被访部门", "访问区域", |
| | | "车牌号", "来访事由", "有效期开始时间", "有效期结束时间", "状态", "登记时间" |
| | | }; |
| | | int[] columnWidths = {20, 16, 24, 16, 16, 20, 24, 16, 24, 22, 22, 20, 22}; |
| | | Sheet sheet = workbook.createSheet("访客记录"); |
| | | sheet.createFreezePane(0, 1); |
| | | CellStyle headerStyle = createHeaderStyle(workbook); |
| | | Row headerRow = sheet.createRow(0); |
| | | for (int index = 0; index < headers.length; index++) |
| | | { |
| | | Cell cell = headerRow.createCell(index); |
| | | cell.setCellValue(headers[index]); |
| | | cell.setCellStyle(headerStyle); |
| | | sheet.setColumnWidth(index, columnWidths[index] * 256); |
| | | } |
| | | |
| | | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | for (int index = 0; index < records.size(); index++) |
| | | { |
| | | SysVisitorRecordVO record = records.get(index); |
| | | Row row = sheet.createRow(index + 1); |
| | | setCellValue(row, 0, record.getRegisterNo()); |
| | | setCellValue(row, 1, record.getVisitorName()); |
| | | setCellValue(row, 2, record.getIdCard()); |
| | | setCellValue(row, 3, registerTypeName(record.getRegisterType())); |
| | | setCellValue(row, 4, record.getIntervieweeName()); |
| | | setCellValue(row, 5, record.getIntervieweeDept()); |
| | | setCellValue(row, 6, record.getVisitAreaNames()); |
| | | setCellValue(row, 7, record.getPlateNo()); |
| | | setCellValue(row, 8, record.getVisitReason()); |
| | | setCellValue(row, 9, formatDate(dateFormat, record.getValidStartTime())); |
| | | setCellValue(row, 10, formatDate(dateFormat, record.getValidEndTime())); |
| | | setCellValue(row, 11, statusName(record.getStatus())); |
| | | setCellValue(row, 12, formatDate(dateFormat, record.getRegisterTime())); |
| | | } |
| | | } |
| | | |
| | | private CellStyle createHeaderStyle(SXSSFWorkbook workbook) |
| | | { |
| | | Font font = workbook.createFont(); |
| | | font.setBold(true); |
| | | CellStyle style = workbook.createCellStyle(); |
| | | style.setFont(font); |
| | | style.setAlignment(HorizontalAlignment.CENTER); |
| | | return style; |
| | | } |
| | | |
| | | private void setCellValue(Row row, int columnIndex, String value) |
| | | { |
| | | row.createCell(columnIndex).setCellValue(value == null ? "" : value); |
| | | } |
| | | |
| | | private String formatDate(SimpleDateFormat dateFormat, java.util.Date date) |
| | | { |
| | | return date == null ? "" : dateFormat.format(date); |
| | | } |
| | | |
| | | private String registerTypeName(String registerType) |
| | | { |
| | | if ("APPOINTMENT".equalsIgnoreCase(registerType)) |
| | | { |
| | | return "预约登记"; |
| | | } |
| | | if ("ONSITE".equalsIgnoreCase(registerType)) |
| | | { |
| | | return "现场登记"; |
| | | } |
| | | return registerType; |
| | | } |
| | | |
| | | private String statusName(String status) |
| | | { |
| | | if ("VISITING".equalsIgnoreCase(status)) |
| | | { |
| | | return "来访中"; |
| | | } |
| | | if ("SIGNED_OUT".equalsIgnoreCase(status)) |
| | | { |
| | | return "已签离"; |
| | | } |
| | | if ("PENDING_CONFIRM".equalsIgnoreCase(status)) |
| | | { |
| | | return "逾期滞留"; |
| | | } |
| | | if ("OVERDUE_AUTO_SIGNED_OUT".equalsIgnoreCase(status)) |
| | | { |
| | | return "超期自动签离"; |
| | | } |
| | | if ("CANCELLED".equalsIgnoreCase(status)) |
| | | { |
| | | return "已取消"; |
| | | } |
| | | return status; |
| | | } |
| | | |
| | | /** |
| | | * 查询访客记录详情 |
| | | */ |
| | | @ApiOperation( |
| | |
| | | public List<SysVisitorRecordVO> pageList(@Param("query") SysVisitorRecordPageRequest query, |
| | | @Param("pageInfo") PageInfo<SysVisitorRecordVO> pageInfo); |
| | | |
| | | public List<SysVisitorRecordVO> list(@Param("query") SysVisitorRecordPageRequest query); |
| | | |
| | | public SysVisitorRecordDetailVO selectDetailById(Long recordId); |
| | | |
| | | public List<SysVisitorPassRecordVO> selectPassRecordsByRecordId(Long recordId); |
| | |
| | | import com.ruoyi.system.vo.SysVisitorRecordDetailVO; |
| | | import com.ruoyi.system.vo.SysVisitorRecordVO; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 访客登记记录服务 |
| | | * |
| | |
| | | { |
| | | public PageInfo<SysVisitorRecordVO> pageList(SysVisitorRecordPageRequest request); |
| | | |
| | | public List<SysVisitorRecordVO> list(SysVisitorRecordPageRequest request); |
| | | |
| | | public SysVisitorRecordDetailVO detail(SysVisitorRecordDetailRequest request); |
| | | |
| | | public int refreshVisitorStatus(); |
| | |
| | | labels.put("plateNo", "车牌号"); |
| | | labels.put("visitReason", "来访事由"); |
| | | labels.put("visitArea", "访问区域"); |
| | | labels.put("receiverName", "接待人"); |
| | | labels.put("registerTime", "登记时间"); |
| | | PRINT_FIELD_LABELS = Collections.unmodifiableMap(labels); |
| | | } |
| | | |
| | |
| | | public PageInfo<SysVisitorRecordVO> pageList(SysVisitorRecordPageRequest request) |
| | | { |
| | | refreshVisitorStatus(); |
| | | SysVisitorRecordPageRequest query = request == null ? new SysVisitorRecordPageRequest() : request; |
| | | query.setIntervieweeName(StringUtils.trim(query.getIntervieweeName())); |
| | | query.setIntervieweeKeyword(StringUtils.trim(query.getIntervieweeKeyword())); |
| | | query.setIntervieweeDept(StringUtils.trim(query.getIntervieweeDept())); |
| | | query.setVisitAreaKeyword(StringUtils.trim(query.getVisitAreaKeyword())); |
| | | query.setStatus(StringUtils.trim(query.getStatus())); |
| | | query.setVisitorName(StringUtils.trim(query.getVisitorName())); |
| | | query.setVisitorKeyword(StringUtils.trim(query.getVisitorKeyword())); |
| | | query.setVisitReason(StringUtils.trim(query.getVisitReason())); |
| | | query.setRegisterType(StringUtils.trim(query.getRegisterType())); |
| | | SysVisitorRecordPageRequest query = normalizeQuery(request); |
| | | PageInfo<SysVisitorRecordVO> pageInfo = new PageInfo<SysVisitorRecordVO>(query.getPageNum(), query.getPageSize()); |
| | | List<SysVisitorRecordVO> list = visitorRecordMapper.pageList(query, pageInfo); |
| | | pageInfo.setRecords(list); |
| | | return pageInfo; |
| | | } |
| | | |
| | | @Override |
| | | public List<SysVisitorRecordVO> list(SysVisitorRecordPageRequest request) |
| | | { |
| | | refreshVisitorStatus(); |
| | | return visitorRecordMapper.list(normalizeQuery(request)); |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | return recordId; |
| | | } |
| | | |
| | | private SysVisitorRecordPageRequest normalizeQuery(SysVisitorRecordPageRequest request) |
| | | { |
| | | SysVisitorRecordPageRequest query = request == null ? new SysVisitorRecordPageRequest() : request; |
| | | query.setIntervieweeName(StringUtils.trim(query.getIntervieweeName())); |
| | | query.setIntervieweeKeyword(StringUtils.trim(query.getIntervieweeKeyword())); |
| | | query.setIntervieweeDept(StringUtils.trim(query.getIntervieweeDept())); |
| | | query.setVisitAreaKeyword(StringUtils.trim(query.getVisitAreaKeyword())); |
| | | query.setStatus(StringUtils.trim(query.getStatus())); |
| | | query.setVisitorName(StringUtils.trim(query.getVisitorName())); |
| | | query.setVisitorKeyword(StringUtils.trim(query.getVisitorKeyword())); |
| | | query.setVisitReason(StringUtils.trim(query.getVisitReason())); |
| | | query.setRegisterType(StringUtils.trim(query.getRegisterType())); |
| | | return query; |
| | | } |
| | | } |
| | |
| | | |
| | | private String deviceId; |
| | | |
| | | private String fingerprint; |
| | | |
| | | private Boolean enabled; |
| | | |
| | | private String onlineStatus; |
| | |
| | | <result property="deviceId" column="device_id" /> |
| | | <result property="enabled" column="enabled" /> |
| | | <result property="onlineStatus" column="online_status" /> |
| | | <result property="fingerprint" column="fingerprint" /> |
| | | <result property="delFlag" column="del_flag" /> |
| | | <result property="remark" column="remark" /> |
| | | <result property="createBy" column="create_by" /> |
| | |
| | | <result property="deviceName" column="device_name" /> |
| | | <result property="brand" column="brand" /> |
| | | <result property="deviceId" column="device_id" /> |
| | | <result property="fingerprint" column="fingerprint" /> |
| | | <result property="enabled" column="enabled" /> |
| | | <result property="onlineStatus" column="online_status" /> |
| | | <result property="remark" column="remark" /> |
| | |
| | | device_id, |
| | | enabled, |
| | | online_status, |
| | | fingerprint, |
| | | del_flag, |
| | | remark, |
| | | create_by, |
| | |
| | | m.device_name, |
| | | m.brand, |
| | | m.device_id, |
| | | m.fingerprint, |
| | | m.enabled, |
| | | m.online_status, |
| | | m.remark, |
| | |
| | | <if test="enabled != null"> |
| | | enabled = #{enabled}, <!-- 注意:enabled 可能是 Boolean 或 Integer,若允许 false 或 0,则只判 null --> |
| | | </if> |
| | | <if test="onlineStatus != null and onlineStatus != ''"> |
| | | online_status = #{onlineStatus}, |
| | | </if> |
| | | <if test="fingerprint != null and fingerprint != ''"> |
| | | fingerprint = #{fingerprint}, |
| | | </if> |
| | | <if test="remark != null and remark != ''"> |
| | | remark = #{remark}, |
| | | </if> |
| | |
| | | from t_visitor_record |
| | | </sql> |
| | | |
| | | <select id="pageList" resultMap="SysVisitorRecordVOResult"> |
| | | <sql id="selectVisitorRecordList"> |
| | | <include refid="selectVisitorRecordBase" /> |
| | | <where> |
| | | and del_flag = '0' |
| | |
| | | ) |
| | | </if> |
| | | <if test="query.visitReason != null and query.visitReason != ''"> |
| | | and visit_reason = #{query.visitReason} |
| | | and visit_reason like concat('%', #{query.visitReason}, '%') |
| | | </if> |
| | | <if test="query.registerType != null and query.registerType != ''"> |
| | | and register_type = #{query.registerType} |
| | | </if> |
| | | </where> |
| | | order by register_time desc, record_id desc |
| | | </sql> |
| | | |
| | | <select id="pageList" resultMap="SysVisitorRecordVOResult"> |
| | | <include refid="selectVisitorRecordList" /> |
| | | </select> |
| | | |
| | | <select id="list" resultMap="SysVisitorRecordVOResult"> |
| | | <include refid="selectVisitorRecordList" /> |
| | | </select> |
| | | |
| | | <select id="selectDetailById" parameterType="Long" resultMap="SysVisitorRecordDetailVOResult"> |
| | |
| | | } |
| | | |
| | | export function getVisitorRecord(recordId) { |
| | | return request({ url: '/system/visitor/record/detail', method: 'post', data: { recordId } }) |
| | | return request({ url: '/system/visitor/record/detail', method: 'post', data: { recordId }}) |
| | | } |
| | | |
| | | export function delVisitorRecord(recordIds) { |
| | | return request({ url: '/system/visitor/record/delete', method: 'post', data: { recordIds } }) |
| | | return request({ url: '/system/visitor/record/delete', method: 'post', data: { recordIds }}) |
| | | } |
| | | |
| | | export function exportVisitorRecord(data) { |
| | | return request({ url: '/system/visitor/record/export', method: 'post', data }) |
| | | } |
| | | |
| | | export function listOperationLog(data) { |
| | |
| | | export function exportOperationLog(data) { |
| | | return request({ url: '/system/operation/log/export', method: 'post', data }) |
| | | } |
| | | |
| | |
| | | isShowTip: { |
| | | type: Boolean, |
| | | default: true |
| | | }, |
| | | // 是否在上传前压缩图片 |
| | | compress: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }, |
| | | data() { |
| | |
| | | this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`); |
| | | return false; |
| | | } |
| | | if (this.fileSize) { |
| | | const isLt = file.size / 1024 / 1024 < this.fileSize; |
| | | if (!isLt) { |
| | | this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`); |
| | | return false; |
| | | const compressedFile = this.compress ? this.compressImage(file) : Promise.resolve(file); |
| | | return compressedFile.then(uploadFile => { |
| | | if (this.fileSize) { |
| | | const isLt = uploadFile.size / 1024 / 1024 < this.fileSize; |
| | | if (!isLt) { |
| | | throw new Error("FILE_SIZE_EXCEEDED"); |
| | | } |
| | | } |
| | | } |
| | | this.$modal.loading("正在上传图片,请稍候..."); |
| | | this.number++; |
| | | this.$modal.loading("正在上传图片,请稍候..."); |
| | | this.number++; |
| | | return uploadFile; |
| | | }).catch(error => { |
| | | const message = error && error.message === "FILE_SIZE_EXCEEDED" |
| | | ? `上传头像图片大小不能超过 ${this.fileSize} MB!` |
| | | : "图片压缩失败,请重试"; |
| | | this.$modal.msgError(message); |
| | | return Promise.reject(error); |
| | | }); |
| | | }, |
| | | // 将人脸图片限制在合理尺寸并转为 JPEG,减少上传和设备同步数据量 |
| | | compressImage(file) { |
| | | return new Promise((resolve, reject) => { |
| | | const reader = new FileReader(); |
| | | reader.onload = event => { |
| | | const image = new Image(); |
| | | image.onload = () => { |
| | | const maxWidth = 1280; |
| | | const maxHeight = 1280; |
| | | const scale = Math.min(1, maxWidth / image.width, maxHeight / image.height); |
| | | const resized = scale < 1; |
| | | const canvas = document.createElement("canvas"); |
| | | canvas.width = Math.max(1, Math.round(image.width * scale)); |
| | | canvas.height = Math.max(1, Math.round(image.height * scale)); |
| | | const context = canvas.getContext("2d"); |
| | | if (!context) { |
| | | resolve(file); |
| | | return; |
| | | } |
| | | context.drawImage(image, 0, 0, canvas.width, canvas.height); |
| | | canvas.toBlob(blob => { |
| | | if (!blob || (!resized && blob.size >= file.size)) { |
| | | resolve(file); |
| | | return; |
| | | } |
| | | const fileName = file.name.replace(/\.[^.]+$/, "") + ".jpg"; |
| | | resolve(new File([blob], fileName, { |
| | | type: "image/jpeg", |
| | | lastModified: Date.now() |
| | | })); |
| | | }, "image/jpeg", 0.82); |
| | | }; |
| | | image.onerror = reject; |
| | | image.src = event.target.result; |
| | | }; |
| | | reader.onerror = reject; |
| | | reader.readAsDataURL(file); |
| | | }); |
| | | }, |
| | | // 文件个数超出 |
| | | handleExceed() { |
| | |
| | | }, |
| | | passwordRules: { |
| | | oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }], |
| | | newPassword: [{ validator: strongPassword, trigger: 'blur' }], |
| | | newPassword: [{ required: true, validator: strongPassword, trigger: 'blur' }], |
| | | confirmPassword: [{ required: true, message: '请确认新密码', trigger: 'blur' }, { validator: confirmPassword, trigger: 'blur' }] |
| | | } |
| | | } |
| | |
| | | import { getToken } from '@/utils/auth' |
| | | import errorCode from '@/utils/errorCode' |
| | | import { tansParams, blobValidate } from "@/utils/ruoyi"; |
| | | import cache from '@/plugins/cache' |
| | | import { saveAs } from 'file-saver' |
| | | import visitorAdminConfig from '@/config/visitor-admin' |
| | | |
| | |
| | | config.baseURL = getVisitorApiBaseUrl() |
| | | // 是否需要设置 token |
| | | const isToken = (config.headers || {}).isToken === false |
| | | // 是否需要防止数据重复提交 |
| | | const isRepeatSubmit = (config.headers || {}).repeatSubmit === false |
| | | if (getToken() && !isToken) { |
| | | config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 |
| | | } |
| | |
| | | url = url.slice(0, -1); |
| | | config.params = {}; |
| | | config.url = url; |
| | | } |
| | | if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { |
| | | const requestObj = { |
| | | url: config.url, |
| | | data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, |
| | | time: new Date().getTime() |
| | | } |
| | | const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小 |
| | | const limitSize = 5 * 1024 * 1024; // 限制存放数据5M |
| | | if (requestSize >= limitSize) { |
| | | console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。') |
| | | return config; |
| | | } |
| | | const sessionObj = cache.session.getJSON('sessionObj') |
| | | if (sessionObj === undefined || sessionObj === null || sessionObj === '') { |
| | | cache.session.setJSON('sessionObj', requestObj) |
| | | } else { |
| | | const s_url = sessionObj.url; // 请求地址 |
| | | const s_data = sessionObj.data; // 请求数据 |
| | | const s_time = sessionObj.time; // 请求时间 |
| | | const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交 |
| | | if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { |
| | | const message = '数据正在处理,请勿重复提交'; |
| | | console.warn(`[${s_url}]: ` + message) |
| | | return Promise.reject(new Error(message)) |
| | | } else { |
| | | cache.session.setJSON('sessionObj', requestObj) |
| | | } |
| | | } |
| | | } |
| | | return config |
| | | }, error => { |
| | |
| | | <el-table-column label="设备名称" prop="deviceName" min-width="120" show-overflow-tooltip /> |
| | | <el-table-column label="品牌" prop="brand" min-width="100" show-overflow-tooltip /> |
| | | <el-table-column label="设备ID" prop="deviceId" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column v-if="deviceType === 'machine'" label="百度SDK指纹" prop="fingerprint" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="所属区域" prop="areaName" min-width="130" show-overflow-tooltip /> |
| | | <el-table-column label="启用状态" align="center" width="100"> |
| | | <template slot-scope="scope"> |
| | |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="人脸图片" prop="faceImage" class="employee-face-upload"> |
| | | <image-upload v-model="form.faceImage" :limit="1" :file-size="5" :is-show-tip="false" /> |
| | | <image-upload v-model="form.faceImage" :limit="1" :file-size="5" :is-show-tip="false" :compress="true" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div slot="footer" class="dialog-footer"> |
| | |
| | | <header class="visitor-header"> |
| | | <h1>访客登记记录</h1> |
| | | </header> |
| | | <el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" class="visitor-query"> |
| | | <el-form ref="queryForm" :model="queryParams" size="small" :inline="true" class="visitor-query"> |
| | | <el-form-item label="访客姓名" prop="visitorName"> |
| | | <el-input v-model="queryParams.visitorName" placeholder="请输入访客姓名" clearable @keyup.enter.native="handleQuery" /> |
| | | </el-form-item> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="被访部门" prop="intervieweeDept"> |
| | | <el-input v-model="queryParams.intervieweeDept" placeholder="请输入被访部门" clearable @keyup.enter.native="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="来访事由" prop="visitReason"> |
| | | <el-input v-model="queryParams.visitReason" placeholder="请输入来访事由" clearable @keyup.enter.native="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="访问区域" prop="areaId"> |
| | | <treeselect v-model="queryParams.areaId" :options="areaOptions" placeholder="请选择区域" clearable class="query-tree" /> |
| | |
| | | </el-form> |
| | | |
| | | <el-row :gutter="10" class="mb8"> |
| | | <el-col v-if="!isMiddlePlatformReadonly" v-hasPermi="['visitor:record:remove']" :span="1.5"> |
| | | <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete()">删除</el-button> |
| | | <el-col v-hasPermi="['visitor:record:export']" :span="1.5"> |
| | | <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button> |
| | | </el-col> |
| | | <right-toolbar :show-search.sync="showSearch" @queryTable="getList" /> |
| | | <el-col v-if="!isMiddlePlatformReadonly" v-hasPermi="['visitor:record:remove']" :span="1.5"> |
| | | <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">批量删除</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-table v-loading="loading" class="visitor-table" :data="recordList" @selection-change="handleSelectionChange"> |
| | | <el-table-column v-if="!isMiddlePlatformReadonly" type="selection" width="50" align="center" /> |
| | | <el-table-column label="登记编号" prop="registerNo" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column label="访客姓名" prop="visitorName" min-width="150" /> |
| | | <el-table-column label="身份证" min-width="150" show-overflow-tooltip> |
| | | <template slot-scope="scope">{{ maskIdCard(scope.row.idCard) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="登记类型" align="center" min-width="150"> |
| | | <template slot-scope="scope">{{ optionLabel(registerTypes, scope.row.registerType) }}</template> |
| | | </el-table-column> |
| | |
| | | <el-table-column label="被访部门" prop="intervieweeDept" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column label="访问区域" prop="visitAreaNames" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column label="车牌号" prop="plateNo" min-width="150" /> |
| | | <el-table-column label="来访事由" prop="visitReason" min-width="150" show-overflow-tooltip /> |
| | | <el-table-column label="有效期" min-width="260" show-overflow-tooltip> |
| | | <template slot-scope="scope">{{ validTimeText(scope.row.validStartTime, scope.row.validEndTime) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" align="center" min-width="150"> |
| | | <template slot-scope="scope"> |
| | | <span class="record-status-pill" :class="statusClass(scope.row.status)">{{ optionLabel(statusOptions, scope.row.status) }}</span> |
| | |
| | | <el-table-column label="登记时间" align="center" prop="registerTime" min-width="150"> |
| | | <template slot-scope="scope">{{ parseTime(scope.row.registerTime) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" align="center" width="150" fixed="right" class-name="small-padding fixed-width"> |
| | | <el-table-column label="操作" align="center" width="90" fixed="right" class-name="small-padding fixed-width"> |
| | | <template slot-scope="scope"> |
| | | <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)">详情</el-button> |
| | | <el-button v-if="!isMiddlePlatformReadonly" v-hasPermi="['visitor:record:remove']" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | </template> |
| | | |
| | | <script> |
| | | import { saveAs } from 'file-saver' |
| | | import Treeselect from '@riophae/vue-treeselect' |
| | | import '@riophae/vue-treeselect/dist/vue-treeselect.css' |
| | | import { treeArea } from '@/api/visitor-admin/area' |
| | | import { listVisitorRecord, delVisitorRecord } from '@/api/visitor-admin/record' |
| | | import { listVisitorRecord, delVisitorRecord, exportVisitorRecord } from '@/api/visitor-admin/record' |
| | | import { getResponseData, getPageRows, getPageTotal, normalizeAreaOptions, toIdArray } from '@/utils/visitor-admin' |
| | | |
| | | export default { |
| | |
| | | data() { |
| | | return { |
| | | loading: false, |
| | | showSearch: true, |
| | | multiple: true, |
| | | ids: [], |
| | | total: 0, |
| | |
| | | } |
| | | return 'is-neutral' |
| | | }, |
| | | // 目的:列表只展示身份证前四位和后四位,中间内容统一脱敏。 |
| | | maskIdCard(value) { |
| | | const text = String(value || '') |
| | | if (!text) { |
| | | return '-' |
| | | } |
| | | if (text.length <= 8) { |
| | | return text |
| | | } |
| | | return text.slice(0, 4) + '*'.repeat(text.length - 8) + text.slice(-4) |
| | | }, |
| | | // 目的:把有效期开始和结束时间合并成列表展示文本。 |
| | | validTimeText(start, end) { |
| | | if (!start && !end) { |
| | | return '-' |
| | | } |
| | | return (this.parseTime(start) || '-') + ' 至 ' + (this.parseTime(end) || '-') |
| | | }, |
| | | // 目的:从第一页重新执行访客记录查询。 |
| | | handleQuery() { |
| | | this.queryParams.pageNum = 1 |
| | |
| | | this.resetForm('queryForm') |
| | | this.handleQuery() |
| | | }, |
| | | // 目的:导出当前筛选条件下的访客登记记录。 |
| | | handleExport() { |
| | | exportVisitorRecord(this.buildQuery()).then(res => { |
| | | const rows = Array.isArray(getResponseData(res)) ? getResponseData(res) : [] |
| | | const blob = new Blob([this.buildExcelXml(rows)], { type: 'application/vnd.ms-excel;charset=utf-8' }) |
| | | saveAs(blob, `访客登记记录_${this.parseTime(new Date(), '{y}{m}{d}{h}{i}{s}')}.xls`) |
| | | }) |
| | | }, |
| | | // 目的:把访客登记记录转换成 Excel 2003 XML,保持与操作日志导出方式一致。 |
| | | buildExcelXml(rows) { |
| | | const columns = [ |
| | | { title: '登记编号', key: 'registerNo', width: 150 }, |
| | | { title: '访客姓名', key: 'visitorName', width: 100 }, |
| | | { title: '证件号码', key: 'idCard', width: 170 }, |
| | | { title: '登记类型', value: row => this.optionLabel(this.registerTypes, row.registerType), width: 90 }, |
| | | { title: '被访人', key: 'intervieweeName', width: 100 }, |
| | | { title: '被访部门', key: 'intervieweeDept', width: 130 }, |
| | | { title: '访问区域', key: 'visitAreaNames', width: 150 }, |
| | | { title: '车牌号', key: 'plateNo', width: 100 }, |
| | | { title: '来访事由', key: 'visitReason', width: 180 }, |
| | | { title: '有效期开始时间', value: row => this.parseTime(row.validStartTime) || '', width: 150 }, |
| | | { title: '有效期结束时间', value: row => this.parseTime(row.validEndTime) || '', width: 150 }, |
| | | { title: '状态', value: row => this.optionLabel(this.statusOptions, row.status), width: 100 }, |
| | | { title: '登记时间', value: row => this.parseTime(row.registerTime) || '', width: 150 } |
| | | ] |
| | | const columnXml = columns.map(column => `<Column ss:Width="${column.width}"/>`).join('') |
| | | const headerXml = `<Row>${columns.map(column => this.excelCell(column.title, 'Header')).join('')}</Row>` |
| | | const bodyXml = rows.map(row => `<Row>${columns.map(column => { |
| | | const value = column.value ? column.value(row) : row[column.key] |
| | | return this.excelCell(value) |
| | | }).join('')}</Row>`).join('') |
| | | return `<?xml version="1.0" encoding="UTF-8"?> |
| | | <?mso-application progid="Excel.Sheet"?> |
| | | <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" |
| | | xmlns:o="urn:schemas-microsoft-com:office:office" |
| | | xmlns:x="urn:schemas-microsoft-com:office:excel" |
| | | xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"> |
| | | <Styles> |
| | | <Style ss:ID="Header"><Font ss:Bold="1"/><Interior ss:Color="#EAF1F9" ss:Pattern="Solid"/></Style> |
| | | </Styles> |
| | | <Worksheet ss:Name="访客登记记录"> |
| | | <Table>${columnXml}${headerXml}${bodyXml}</Table> |
| | | </Worksheet> |
| | | </Workbook>` |
| | | }, |
| | | // 目的:生成 Excel 文本单元格并按需应用表头样式。 |
| | | excelCell(value, styleId) { |
| | | const style = styleId ? ` ss:StyleID="${styleId}"` : '' |
| | | return `<Cell${style}><Data ss:Type="String">${this.escapeXml(value)}</Data></Cell>` |
| | | }, |
| | | // 目的:转义 Excel XML 特殊字符,避免访客数据破坏文件结构。 |
| | | escapeXml(value) { |
| | | return String(value == null ? '' : value).replace(/[<>&'"]/g, char => ({ |
| | | '<': '<', |
| | | '>': '>', |
| | | '&': '&', |
| | | "'": ''', |
| | | '"': '"' |
| | | }[char])) |
| | | }, |
| | | // 目的:同步表格勾选状态,控制批量删除按钮可用性。 |
| | | handleSelectionChange(selection) { |
| | | this.ids = selection.map(item => item.recordId) |
| | |
| | | handleDetail(row) { |
| | | this.$router.push(`/visitor/record/detail/${row.recordId}`) |
| | | }, |
| | | // 目的:删除访客记录;中台只读模式下禁止本地删除。 |
| | | handleDelete(row) { |
| | | // 目的:批量删除已勾选的访客记录;中台只读模式下禁止本地删除。 |
| | | handleDelete() { |
| | | if (this.isMiddlePlatformReadonly) { |
| | | this.denyMiddlePlatformWrite() |
| | | return |
| | | } |
| | | const ids = row ? [row.recordId] : toIdArray(this.ids) |
| | | this.$modal.confirm('是否确认删除选中的访客登记记录?').then(() => delVisitorRecord(ids)).then(() => { |
| | | const ids = toIdArray(this.ids) |
| | | this.$modal.confirm(`是否确认批量删除选中的 ${ids.length} 条访客登记记录?`).then(() => delVisitorRecord(ids)).then(() => { |
| | | this.$modal.msgSuccess('删除成功') |
| | | this.getList() |
| | | }) |
| | |
| | | { fieldCode: 'visitAreaNames', fieldName: '访问区域' }, |
| | | { fieldCode: 'validTime', fieldName: '有效期' }, |
| | | { fieldCode: 'plateNo', fieldName: '车牌号' }, |
| | | { fieldCode: 'visitReason', fieldName: '来访事由' } |
| | | { fieldCode: 'visitReason', fieldName: '来访事由' }, |
| | | { fieldCode: 'receiverName', fieldName: '接待人' }, |
| | | { fieldCode: 'registerTime', fieldName: '登记时间' } |
| | | ] |
| | | } |
| | | }, |
| | |
| | | visitAreaNames: 'A区 / 办公楼', |
| | | validTime: '2026-07-07 09:00~18:00', |
| | | plateNo: '苏A12345', |
| | | visitReason: '商务拜访' |
| | | visitReason: '商务拜访', |
| | | receiverName: '王五', |
| | | registerTime: '2026-07-23 09:00' |
| | | } |
| | | return this.printFieldOptions |
| | | .filter(item => this.form.printFields.indexOf(item.fieldCode) !== -1) |
| | |
| | | proxy: { |
| | | // detail: https://cli.vuejs.org/config/#devserver-proxy |
| | | [process.env.VUE_APP_BASE_API]: { |
| | | target: `http://localhost:9098`, |
| | | target: `http://221.182.45.100:9799`, |
| | | changeOrigin: true, |
| | | pathRewrite: { |
| | | ['^' + process.env.VUE_APP_BASE_API]: '' |
| | | } |
| | | }, |
| | | '/profile': { |
| | | target: `http://localhost:9098`, |
| | | target: `http://221.182.45.100:9799`, |
| | | changeOrigin: true |
| | | } |
| | | }, |
| | |
| | | .machine-table th:nth-child(8), |
| | | .machine-table td:nth-child(8), |
| | | .machine-table th:nth-child(9), |
| | | .machine-table td:nth-child(9) { |
| | | width: 11.111%; |
| | | .machine-table td:nth-child(9), |
| | | .machine-table th:nth-child(10), |
| | | .machine-table td:nth-child(10) { |
| | | width: 10%; |
| | | } |
| | |
| | | ' <td>' + escapeHtml(item.deviceName || "-") + '</td>', |
| | | ' <td>' + escapeHtml(item.brand || "-") + '</td>', |
| | | ' <td>' + escapeHtml(item.deviceId || "-") + '</td>', |
| | | ' <td title="' + escapeHtml(item.fingerprint || "") + '">' + escapeHtml(item.fingerprint || "-") + '</td>', |
| | | ' <td>' + escapeHtml(item.areaName || "-") + '</td>', |
| | | ' <td>' + renderEnabledStatus(item.enabled) + '</td>', |
| | | ' <td>' + renderOnlineStatus(item.onlineStatus) + '</td>', |
| | |
| | | if (text.length < 8) { |
| | | return text || "-"; |
| | | } |
| | | return text.slice(0, 6) + "********" + text.slice(-4); |
| | | return text.slice(0, 4) + repeatString("*", text.length - 8) + text.slice(-4); |
| | | } |
| | | |
| | | function repeatString(value, count) { |
| | | var result = ""; |
| | | for (var index = 0; index < count; index++) { |
| | | result += value; |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | function toServerDateTime(value) { |
| | |
| | | <th>设备名称</th> |
| | | <th>品牌</th> |
| | | <th>设备ID</th> |
| | | <th>百度SDK指纹</th> |
| | | <th>所属区域</th> |
| | | <th>启用状态</th> |
| | | <th>在线状态</th> |