Parcourir la source

feat: 优化 NFC 页面布局和读卡器客户端逻辑

ruhuxu il y a 1 semaine
Parent
commit
062b0d8fef
4 fichiers modifiés avec 514 ajouts et 473 suppressions
  1. 5 1
      js/reader/ka-nfc-page.js
  2. 68 31
      js/reader/reader-client.js
  3. 3 3
      page/biz/ka_nfcAdd.ss.jsp
  4. 438 438
      page/ka_nfcAdd.jsp

+ 5 - 1
js/reader/ka-nfc-page.js

@@ -98,8 +98,12 @@
     seenUids.set(uid, { protocol: protocol, time: Date.now() });
   }
 
+  function getNfcInput() {
+    return document.querySelector('input[name="kah"]');
+  }
+
   function setNfcValue(uid) {
-    var input = getEl("nfch");
+    var input = getNfcInput();
     if (!input) return;
     input.value = uid;
     input.dispatchEvent(new Event("input", { bubbles: true }));

+ 68 - 31
js/reader/reader-client.js

@@ -221,6 +221,52 @@ class ReaderClient {
     return [];
   }
 
+  buildISO14443AReadParam(readDataType) {
+    return {
+      antennaCollection: [1],
+      readDataType: String(readDataType),
+      // 厂家服务对 -1 的实现不稳定,仍带齐参数避免服务端空引用
+      keyType: this.keyType,
+      key: this.key,
+      blockAddress: this.blockAddress,
+      numberOfBlocksRead: this.numberOfBlocksRead,
+    };
+  }
+
+  async sendISO14443ARead(readDataType) {
+    return await this.sendCommand(
+      "ISO14443A_BatchReadMultiBlocks",
+      this.buildISO14443AReadParam(readDataType),
+      this.handle,
+      6000
+    );
+  }
+
+  extractUidFromReadResponse(readResp, emptyHint) {
+    if (typeof readResp === "string") {
+      throw new Error(readResp.trim() || "ISO14443A 读卡失败");
+    }
+
+    const code = this.getPayloadCode(readResp);
+    if (code != null && code < 0) {
+      const msg = this.getPayloadMessage(readResp) || "ISO14443A 读卡失败";
+      throw new Error(msg);
+    }
+
+    const tagDataList = this.extractTagDataList(readResp);
+    if (!Array.isArray(tagDataList) || tagDataList.length === 0) {
+      throw new Error(emptyHint);
+    }
+
+    const firstRow = tagDataList[0];
+    const uid = this.parseUidFromTagRow(firstRow);
+    if (!uid) {
+      throw new Error(emptyHint);
+    }
+
+    return uid.toUpperCase();
+  }
+
   parseUidFromTagRow(row) {
     if (!row) {
       return "";
@@ -299,38 +345,29 @@ class ReaderClient {
       throw new Error("读卡器未连接,请先连接读卡器");
     }
 
-    const readResp = await this.sendCommand(
-      "ISO14443A_BatchReadMultiBlocks",
-      {
-        antennaCollection: [1],
-        readDataType: "0",
-        keyType: this.keyType,
-        key: this.key,
-        blockAddress: this.blockAddress,
-        numberOfBlocksRead: this.numberOfBlocksRead,
-      },
-      this.handle,
-      6000
-    );
-
-    const code = this.getPayloadCode(readResp);
-    if (code != null && code < 0) {
-      const msg = this.getPayloadMessage(readResp) || "ISO14443A 读卡失败";
-      throw new Error(msg);
-    }
-
-    const tagDataList = this.extractTagDataList(readResp);
-    if (!Array.isArray(tagDataList) || tagDataList.length === 0) {
-      throw new Error("未读取到 UID,请确认 ISO14443A 卡片已到读卡位");
-    }
-
-    const firstRow = tagDataList[0];
-    const uid = this.parseUidFromTagRow(firstRow);
-    if (!uid) {
-      throw new Error("未读取到 UID,请确认 ISO14443A 卡片已到读卡位");
+    try {
+      const inventoryResp = await this.sendISO14443ARead("-1");
+      return this.extractUidFromReadResponse(
+        inventoryResp,
+        "未读取到 UID,请确认 ISO14443A 卡片已到读卡位"
+      );
+    } catch (inventoryErr) {
+      const fallbackResp = await this.sendISO14443ARead("0");
+      try {
+        return this.extractUidFromReadResponse(
+          fallbackResp,
+          "未读取到 UID,请确认 ISO14443A 卡片已到读卡位"
+        );
+      } catch (fallbackErr) {
+        const inventoryMsg =
+          inventoryErr instanceof Error ? inventoryErr.message : String(inventoryErr || "");
+        const fallbackMsg =
+          fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr || "");
+        throw new Error(
+          `ISO14443A 读取失败;只盘点模式:${inventoryMsg || "未知错误"};读块回退模式:${fallbackMsg || "未知错误"}`
+        );
+      }
     }
-
-    return uid.toUpperCase();
   }
 
   // ISO15693 读取 UID

+ 3 - 3
page/biz/ka_nfcAdd.ss.jsp

@@ -92,7 +92,9 @@
 	</div>
 
 
-	<div class='bottom-div'>
+	
+</div>
+<div class='bottom-div'>
 
 		<ss-bottom-button
 				id="saveAndCommit"
@@ -109,8 +111,6 @@
 		></ss-bottom-button>
 
 	</div>
-</div>
-
 </form>
 </body>
 

+ 438 - 438
page/ka_nfcAdd.jsp

@@ -1,438 +1,438 @@
-<%@ page language="java" pageEncoding="UTF-8" isELIgnored="false" %>
-<%@ taglib uri="/ssTag" prefix="ss"%>
-
-<% pageContext.setAttribute(ss.page.PageC.PAGE_objName,"ka");%>
-<%pageContext.setAttribute("wdpageinformation","{'hastab':'0'}");%>
-<!DOCTYPE html>
-<html>
-<head>
-<%@ include file="/page/clip/header.jsp" %>
-
-	<style>
-	  .table-container>tr>th{
-		  width:130px !important;
-	  }
-	  /* 把content-box的高度限制 从公共css 抽到具体有需要的页面 by xu 20251215 */
-	  .form-container .content-box {
-		  height: calc(100% - 80px) !important;
-		      padding-top: 40px !important;
-	  }
-	  td{
-		display: flex;
-		align-items: center;
-		justify-content: flex-start;
-		}
-	</style>
-
-</head>
-<body class="env-input-body">
-<script src="/js/reader/reader-client.js"></script>
-<div style="position:fixed;top:0px;left:0px;z-index:9999;padding:8px 24px;display:flex;align-items:center;gap:12px;">
-	<button id="btnConnect" class="primary" type="button" style="display:none;">连接读卡器</button>
-	<span id="readerStatusTag" style="font-size:14px;color:#64748b;">读卡器连接中...</span>
-</div>
-<input id="wsUrlText" type="hidden" value="ws://127.0.0.1:6689"/> <%-- WebSocket。Lin --%>
-<input id="pollingInterval" type="hidden" value="1000" min="100" step="100" /> <%-- 轮询间隔 (毫秒)。Lin --%>
-<input id="dedupeMode" type="hidden" value="session"/> <%-- 去重模式:本次会话去重。Lin --%>
-<%-- ISO14443A 参数。Lin --%>
-<input id="keyTypeInput" type="hidden" value="0"/> <%-- 密钥类型:Key A。1 = Key B。Lin --%>
-<input id="keyInput" type="hidden" value="FFFFFFFFFFFF" maxlength="12"/> <%-- 密钥。Lin --%>
-<input id="blockAddressInput" type="hidden" value="1" min="0" /> <%-- 块地址。Lin --%>
-<input id="numberOfBlocksReadInput" type="hidden" value="1" min="1"/> <%-- 读取块数。Lin --%>
-<%-- --%>
-<%-- ISO15693 参数。Lin --%>
-<input id="readDataTypeInput" type="hidden" value="0"/> <%-- 读取数据类型:block。1 = afi,2 = eas,3 = block and afi,4 = block and eas,5 = afi and eas,6 = block and afi and eas。Lin --%>
-<input id="readSecurityStatusInput" type="hidden" value="0"/> <%-- 读取安全位:不读取。1 = 读取。Lin --%>
-<input id="iso15693BlockAddressInput" type="hidden" value="0" min="0"/> <%-- 块地址。Lin --%>
-<input id="iso15693NumberOfBlocksInput" type="hidden" value="7" min="1"/> <%-- 读取块数。Lin --%>
-<%-- --%>
-<form method="post" id="app" class="form-container">
-<div class="content-box fit-height-content">
-
-	<div class="content-div" ssFith="true">
-		<table class='form'>
-
-			<tr>
-				<th>部门/亲属</th>
-				<td style="display: flex;align-items: center;">
-					
-<script>
-ss.dom.formElemConfig.bmid={val:null,type:window.ss.dom.TYPE.OBJP};
-</script>
-<ss-objp
-:opt="bmidOption"
-:inp="true"
-url="<ss:serv name='loadObjpOpt' parm='{"objectpickerdropdown":"1"}' />"
-cb="bm"
-v-model="bmid"
-name="bmid"
-:readonly="false"
-width="200px"
-></ss-objp>
-
-					
-<script>
-ss.dom.formElemConfig.rylbm={val:'1100',type:window.ss.dom.TYPE.ONOFFBTN};
-</script>
-
-					<ss-onoff
-v-model="rylbm"
-name="rylbm"
-label="职工亲属"
-value="1000"
-:multiple="true"
-:null="false"
-placeholder="职工亲属"
-v-model="rylbm"
-:readonly="false"
-></ss-onoff>
-
-				</td>
-			</tr>
-
-<%-- 先去掉,接入读卡器时再加。Lin
-			<tr>
-				<th>卡号</th>
-				<td >
-					<@input name="kah"/>
-				</td>
-			</tr>
---%>
-
-			<tr>
-				<th>人员</th>
-				<td>
-					<input name="czryid" type="hidden" value='${sessionScope.ssUser.ryid}'/> <%-- 操作人员ID。Lin --%>
-
-					
-<script>
-ss.dom.formElemConfig.cyryid={val:null,type:window.ss.dom.TYPE.OBJP};
-</script>
-<ss-objp
-:opt="cyryidOption"
-:inp="true"
-url="<ss:serv name='loadObjpOpt' parm='{"objectpickerdropdown":"1","objectpickerfilterField":"bmid,rylbm"}' />"
-cb="ryByBmOrRylb"
-v-model="cyryid"
-name="cyryid"
-:readonly="false"
-filterField="bmid,rylbm"
-onChange="selBaseInfoByRyid"
-></ss-objp>
-
-				</td>
-			
-				<th>部门</th>
-				<td id='bm'></td>
-			</tr>
-			<tr>
-				<th>姓名</th>
-				<td id='xm'></td>
-			
-				<th>人员号</th>
-				<td id='ryh'></td>
-			</tr>
-			<tr>
-				<th>NFC</th>
-				<td>
-					<input onChange="selBaseInfoByNfch" name="kah"/>
-				</td>
-			</tr>
-		</table>
-	</div>
-
-
-	
-</div>
-<div class='bottom-div'>
-
-		<ss-bottom-button
-				id="saveAndCommit"
-				text="保存并提交"
-				onclick='submitGrtfForm();'<%-- 功能说明:个人退费页提交前先校验金额必须为负数 by xu 20260323 --%>
-				icon-class="bottom-div-save"
-		></ss-bottom-button>
-
-
-		<ss-bottom-button
-				text="关闭"
-				onclick='ss.display.closeDialog();'
-				icon-class="bottom-div-close"
-		></ss-bottom-button>
-
-	</div>
-<input name='wdComponentID' type='hidden' value='ka_nfcAdd'/></form>
-<script type="text/javascript">var wdRecordValue='${wdRecordValue}';</script>
-<script type="text/javascript" src="/ss/js/wdRecord.js"></script>
-<script type="text/javascript">(function(){wdRecord("ka_nfcAdd");})();</script>
-<script type="text/javascript" src="/ss/js/wdFitHeight.js"></script>
-<script type="text/javascript">initWdFitHeight(0)</script>
-<script type="text/javascript">initWdFitHeightFunction=function(){initWdFitHeight(0);};</script>
-<ss:equal val="${empty resizeComponent}" val2="false">
-<script>{var iframe=wd.display.getFrameOfWindow();
-if(iframe&&iframe.contentWindow==window)
-wd.display.resizeComponent(${resizeComponent.width}, ${resizeComponent.height}, ${empty resizeComponent.minHeight?'null':resizeComponent.minHeight}, ${empty resizeComponent.maxHeight?'null':resizeComponent.maxHeight});}</script>
-</ss:equal>
-<ss:help/>
-</body>
-<script type="text/javascript">
-try{wd.display.showMsgPopup('${msg}');
-}catch(err){console.error(err);}
-</script>
-<ss:equal val="${empty wdclosewindowparam}" val2="false">
-<script type="text/javascript">
-try{wd.display.setCloseWindowParam('${wdclosewindowparam}');
-}catch(err){console.error(err);}
-</script>
-</ss:equal>
-
-</html>
-<%@ include file="/page/clip/footer.jsp" %>
-
-<script>
-	function getFormAppVm(){
-		var appEl = document.getElementById("app");
-		if (!appEl || !appEl.__vue_app__ || !appEl.__vue_app__._instance) {
-			return null;
-		}
-		return appEl.__vue_app__._instance.proxy || null;
-	}
-
-	function normalizeRylbmValues(value){
-		if (Array.isArray(value)) {
-			return value.map(function(item){
-				return item == null ? "" : item.toString();
-			}).filter(Boolean);
-		}
-		if (value == null || value === "") {
-			return [];
-		}
-		var cleanValue = value.toString().replace(/^,+/, "");
-		if (!cleanValue) {
-			return [];
-		}
-		return cleanValue.split(/[,|]/).filter(Boolean);
-	}
-
-	function hasRelativeRylbmValue(value){
-		return normalizeRylbmValues(value).indexOf("1000") !== -1;
-	}
-
-	function clearRyDisplay(){
-		var bmbjEl = document.getElementById('bmbj');
-		var xmEl = document.getElementById('xm');
-		var ryhEl = document.getElementById('ryh');
-		if (bmbjEl) bmbjEl.innerHTML = "";
-		if (xmEl) xmEl.innerHTML = "";
-		if (ryhEl) ryhEl.innerHTML = "";
-	}
-
-	function clearRySelection(vm){
-		if (!vm) {
-			clearRyDisplay();
-			return;
-		}
-		vm.ryid = "";
-		clearRyDisplay();
-	}
-
-	function handleRylbmChange(groupValue){
-		if (!hasRelativeRylbmValue(groupValue)) {
-			return;
-		}
-		var vm = getFormAppVm();
-		if (!vm) {
-			clearRyDisplay();
-			return;
-		}
-		vm.bmid = "";
-		clearRySelection(vm);
-	}
-
-	function handleBjChange(value){
-		if (value == null || value === "") {
-			return;
-		}
-		var vm = getFormAppVm();
-		if (!vm) {
-			clearRyDisplay();
-			return;
-		}
-		if (hasRelativeRylbmValue(vm.rylbm)) {
-			vm.rylbm = "1100";
-		}
-		clearRySelection(vm);
-	}
-	
-	function selBaseInfoByRyid(value){
-		$.ajax({
-			url:"<ss:serv name='ry_selBaseInfoByRyid'/>",
-			type:"post",
-			data:{
-				ryid:value
-			},
-			dataType:"json",
-			success:function(data){
-				if (data.ssCode != 0) {
-					alert(data.ssMsg);
-					return;
-				}
-				var d = data.ssData;
-				document.getElementById('xm').innerHTML = d.xm;
-				document.getElementById('ryh').innerHTML = d.ryh;
-				if (d.rylbm != 1100) {	// 不是学员。Lin
-					if (d.bmmc)
-						document.getElementById('bmbj').innerHTML = d.bmmc;
-					else
-						document.getElementById('bmbj').innerHTML = "(无)";
-				} else {
-					if (d.bjmc)
-						document.getElementById('bmbj').innerHTML = d.bjmc;
-					else
-						document.getElementById('bmbj').innerHTML = "(无)";
-				}
-				document.getElementById('xfye').innerHTML = d.xfye;
-			}
-		});
-	}
-</script>
-<script type="text/javascript" src="/js/validate/validator-rules.js"></script><%-- 功能说明:个人退费页接入桌面端校验规则,支持 SsInp 红线提示 by xu 20260323 --%>
-<script type="text/javascript" src="/js/validate/validation-manager.js"></script><%-- 功能说明:个人退费页接入桌面端校验管理器,支持 SsInp 红线提示 by xu 20260323 --%>
-
-<script>
-	var GRTF_LABEL_MAP = window.GRTF_LABEL_MAP || {};
-
-	// 功能说明:记录当前退费类别的最新 value/label,避免 onoff 隐藏值切换时机导致金额校验判断失真 by xu 20260323
-	var CURRENT_GRTF_CATEGORY_STATE = {
-		value: "",
-		label: ""
-	};
-
-	// 功能说明:退费类别变化后延后一拍重跑金额校验,确保隐藏字段值更新后再清理红线状态 by xu 20260323
-	function handleGrczlbmChange(groupValue, value, label){
-		CURRENT_GRTF_CATEGORY_STATE.value = groupValue == null ? "" : groupValue.toString();
-		CURRENT_GRTF_CATEGORY_STATE.label = label == null ? "" : label.toString();
-		setTimeout(function(){
-			if (window.ssVm && typeof window.ssVm.validateField === "function") {
-				window.ssVm.validateField("je");
-				return;
-			}
-			validateNegativeAmountByCategory(false);
-		}, 0);
-	}
-
-	// 功能说明:初始化个人退费页金额负数校验规则,整页金额都必须输入负数 by xu 20260323
-	function initNegativeAmountValidation(){
-		if (!document.querySelector('[name="je"]') || !document.querySelector('[name="grczlbm"]')) {
-			return;
-		}
-		if (!window.ssVm || typeof window.ssVm.add !== "function" || window.ss.dom._grczGrtfNegativeAmountValidationInited) {
-			return;
-		}
-		window.ss.dom._grczGrtfNegativeAmountValidationInited = true;
-		window.ssVm.add("ss.commonValidator.custom", ["je"], {
-			msgPrfx: "金额",
-			relField: "grczlbm",
-			validate: function(value, categoryValue){
-				var currentLabel = getCurrentGrczlbmLabel(categoryValue);
-				if (value == null || value.toString().trim() === "") {
-					return true;
-				}
-				if (isValidNegativeAmountText(value)) {
-					return true;
-				}
-				return {
-					valid: false,
-					message: (currentLabel || "退费") + "金额只能输入负数"
-				};
-			}
-		}, {
-			je: window.ss.dom.formElemConfig.je ? window.ss.dom.formElemConfig.je.val : ""
-		});
-	}
-
-	// 功能说明:根据当前退费类别值读取显示文案,供联动校验和错误提示复用 by xu 20260323
-	function getCurrentGrczlbmLabel(categoryValue){
-		var currentValue = categoryValue == null ? getCurrentGrczlbmValue() : categoryValue.toString();
-		if (CURRENT_GRTF_CATEGORY_STATE.label && (!currentValue || CURRENT_GRTF_CATEGORY_STATE.value === currentValue)) {
-			return CURRENT_GRTF_CATEGORY_STATE.label;
-		}
-		return GRTF_LABEL_MAP[currentValue] || "";
-	}
-
-	// 功能说明:统一判断金额文本是否为合法负数,供提交校验和 ssVm 规则复用 by xu 20260323
-	function isValidNegativeAmountText(value){
-		var amountText = value == null ? "" : value.toString().trim();
-		if (!amountText) {
-			return false;
-		}
-		var amountNumber = Number(amountText);
-		return !isNaN(amountNumber) && amountNumber < 0;
-	}
-
-	// 功能说明:根据当前退费类别值读取隐藏字段值,供金额校验复用 by xu 20260323
-	function getCurrentGrczlbmValue(){
-		var vm = getFormAppVm();
-		if (vm && vm.grczlbm != null && vm.grczlbm !== "") {
-			return vm.grczlbm.toString();
-		}
-		var categoryElem = document.querySelector('[name="grczlbm"]');
-		return categoryElem && categoryElem.value != null ? categoryElem.value.toString() : "";
-	}
-
-	// 功能说明:统一校验个人退费金额必须为负数,不再区分具体退费类别 by xu 20260323
-	function validateNegativeAmountByCategory(showMsg){
-		var currentLabel = getCurrentGrczlbmLabel();
-		var jeElem = document.querySelector('[name="je"]');
-		if (!jeElem) {
-			return true;
-		}
-
-		if (isValidNegativeAmountText(jeElem.value)) {
-			return true;
-		}
-
-		if (showMsg !== false) {
-			alert((currentLabel || "退费") + "金额只能输入负数");
-			jeElem.focus();
-		}
-		return false;
-	}
-
-	// 功能说明:个人退费页提交前先走 ssVm 全量校验,确保显示 SsInp 左侧红线与底部提示 by xu 20260323
-	function submitGrtfForm(){
-/* 去掉,不是 个人退费页 了。Lin
-		if (window.ssVm && window.ssVm.validations && window.ssVm.validations.size > 0) {
-			var validateResult = window.ssVm.validateAll();
-			if (!validateResult.valid) {
-				return false;
-			}
-		} else if (!validateNegativeAmountByCategory(true)) {
-			return false;
-		}
-*/
-
-		var formElem = document.querySelector("form");
-		if (!formElem) {
-			alert("表单不存在");
-			return false;
-		}
-
-		formElem.action = "<ss:serv name='ka_lr_tj' dest='addSure' parm='{thisViewObject:\"ka\",dataType:\"update\"}'/>";
-		ss.display.resizeComponent(881,361,515,515);
-		formElem.submit();
-		return true;
-	}
-
-	if (window.SS && typeof SS.ready === "function") {
-		SS.ready(function(){
-			// 功能说明:页面初始化后同步当前退费类别状态,避免首次校验读取不到最新类别文案 by xu 20260323
-			CURRENT_GRTF_CATEGORY_STATE.value = getCurrentGrczlbmValue();
-			CURRENT_GRTF_CATEGORY_STATE.label = GRTF_LABEL_MAP[CURRENT_GRTF_CATEGORY_STATE.value] || "";
-			// 功能说明:页面初始化后注册个人退费金额负数校验规则,保证 SsInp 输入时直接出现红线提示 by xu 20260323
-			initNegativeAmountValidation();
-		});
-	}
-
-</script>
-<script type="text/javascript" src="/js/reader/ka-nfc-page.js"></script>
+<%@ page language="java" pageEncoding="UTF-8" isELIgnored="false" %>
+<%@ taglib uri="/ssTag" prefix="ss"%>
+
+<% pageContext.setAttribute(ss.page.PageC.PAGE_objName,"ka");%>
+<%pageContext.setAttribute("wdpageinformation","{'hastab':'0'}");%>
+<!DOCTYPE html>
+<html>
+<head>
+<%@ include file="/page/clip/header.jsp" %>
+
+	<style>
+	  .table-container>tr>th{
+		  width:130px !important;
+	  }
+	  /* 把content-box的高度限制 从公共css 抽到具体有需要的页面 by xu 20251215 */
+	  .form-container .content-box {
+		  height: calc(100% - 80px) !important;
+		      padding-top: 40px !important;
+	  }
+	  td{
+		display: flex;
+		align-items: center;
+		justify-content: flex-start;
+		}
+	</style>
+
+</head>
+<body class="env-input-body">
+<script src="/js/reader/reader-client.js"></script>
+<script type="text/javascript" src="/js/reader/ka-nfc-page.js"></script>
+<div style="position:fixed;top:0px;left:0px;z-index:9999;padding:8px 24px;display:flex;align-items:center;gap:12px;">
+	<button id="btnConnect" class="primary" type="button" style="display:none;">连接读卡器</button>
+	<span id="readerStatusTag" style="font-size:14px;color:#64748b;">读卡器连接中...</span>
+</div>
+<input id="wsUrlText" type="hidden" value="ws://127.0.0.1:6689"/> <%-- WebSocket。Lin --%>
+<input id="pollingInterval" type="hidden" value="1000" min="100" step="100" /> <%-- 轮询间隔 (毫秒)。Lin --%>
+<input id="dedupeMode" type="hidden" value="session"/> <%-- 去重模式:本次会话去重。Lin --%>
+<%-- ISO14443A 参数。Lin --%>
+<input id="keyTypeInput" type="hidden" value="0"/> <%-- 密钥类型:Key A。1 = Key B。Lin --%>
+<input id="keyInput" type="hidden" value="FFFFFFFFFFFF" maxlength="12"/> <%-- 密钥。Lin --%>
+<input id="blockAddressInput" type="hidden" value="1" min="0" /> <%-- 块地址。Lin --%>
+<input id="numberOfBlocksReadInput" type="hidden" value="1" min="1"/> <%-- 读取块数。Lin --%>
+<%-- --%>
+<%-- ISO15693 参数。Lin --%>
+<input id="readDataTypeInput" type="hidden" value="0"/> <%-- 读取数据类型:block。1 = afi,2 = eas,3 = block and afi,4 = block and eas,5 = afi and eas,6 = block and afi and eas。Lin --%>
+<input id="readSecurityStatusInput" type="hidden" value="0"/> <%-- 读取安全位:不读取。1 = 读取。Lin --%>
+<input id="iso15693BlockAddressInput" type="hidden" value="0" min="0"/> <%-- 块地址。Lin --%>
+<input id="iso15693NumberOfBlocksInput" type="hidden" value="7" min="1"/> <%-- 读取块数。Lin --%>
+<%-- --%>
+<form method="post" id="app" class="form-container">
+<div class="content-box fit-height-content">
+
+	<div class="content-div" ssFith="true">
+		<table class='form'>
+
+			<tr>
+				<th>部门/亲属</th>
+				<td style="display: flex;align-items: center;">
+					
+<script>
+ss.dom.formElemConfig.bmid={val:null,type:window.ss.dom.TYPE.OBJP};
+</script>
+<ss-objp
+:opt="bmidOption"
+:inp="true"
+url="<ss:serv name='loadObjpOpt' parm='{"objectpickerdropdown":"1"}' />"
+cb="bm"
+v-model="bmid"
+name="bmid"
+:readonly="false"
+width="200px"
+></ss-objp>
+
+					
+<script>
+ss.dom.formElemConfig.rylbm={val:'1100',type:window.ss.dom.TYPE.ONOFFBTN};
+</script>
+
+					<ss-onoff
+v-model="rylbm"
+name="rylbm"
+label="职工亲属"
+value="1000"
+:multiple="true"
+:null="false"
+placeholder="职工亲属"
+v-model="rylbm"
+:readonly="false"
+></ss-onoff>
+
+				</td>
+			</tr>
+
+<%-- 先去掉,接入读卡器时再加。Lin
+			<tr>
+				<th>卡号</th>
+				<td >
+					<@input name="kah"/>
+				</td>
+			</tr>
+--%>
+
+			<tr>
+				<th>人员</th>
+				<td>
+					<input name="czryid" type="hidden" value='${sessionScope.ssUser.ryid}'/> <%-- 操作人员ID。Lin --%>
+
+					
+<script>
+ss.dom.formElemConfig.cyryid={val:null,type:window.ss.dom.TYPE.OBJP};
+</script>
+<ss-objp
+:opt="cyryidOption"
+:inp="true"
+url="<ss:serv name='loadObjpOpt' parm='{"objectpickerdropdown":"1","objectpickerfilterField":"bmid,rylbm"}' />"
+cb="ryByBmOrRylb"
+v-model="cyryid"
+name="cyryid"
+:readonly="false"
+filterField="bmid,rylbm"
+onChange="selBaseInfoByRyid"
+></ss-objp>
+
+				</td>
+			
+				<th>部门</th>
+				<td id='bm'></td>
+			</tr>
+			<tr>
+				<th>姓名</th>
+				<td id='xm'></td>
+			
+				<th>人员号</th>
+				<td id='ryh'></td>
+			</tr>
+			<tr>
+				<th>NFC</th>
+				<td>
+					<input onChange="selBaseInfoByNfch" name="kah"/>
+				</td>
+			</tr>
+		</table>
+	</div>
+
+
+	
+</div>
+<div class='bottom-div'>
+
+		<ss-bottom-button
+				id="saveAndCommit"
+				text="保存并提交"
+				onclick='submitGrtfForm();'<%-- 功能说明:个人退费页提交前先校验金额必须为负数 by xu 20260323 --%>
+				icon-class="bottom-div-save"
+		></ss-bottom-button>
+
+
+		<ss-bottom-button
+				text="关闭"
+				onclick='ss.display.closeDialog();'
+				icon-class="bottom-div-close"
+		></ss-bottom-button>
+
+	</div>
+<input name='wdComponentID' type='hidden' value='ka_nfcAdd'/></form>
+<script type="text/javascript">var wdRecordValue='${wdRecordValue}';</script>
+<script type="text/javascript" src="/ss/js/wdRecord.js"></script>
+<script type="text/javascript">(function(){wdRecord("ka_nfcAdd");})();</script>
+<script type="text/javascript" src="/ss/js/wdFitHeight.js"></script>
+<script type="text/javascript">initWdFitHeight(0)</script>
+<script type="text/javascript">initWdFitHeightFunction=function(){initWdFitHeight(0);};</script>
+<ss:equal val="${empty resizeComponent}" val2="false">
+<script>{var iframe=wd.display.getFrameOfWindow();
+if(iframe&&iframe.contentWindow==window)
+wd.display.resizeComponent(${resizeComponent.width}, ${resizeComponent.height}, ${empty resizeComponent.minHeight?'null':resizeComponent.minHeight}, ${empty resizeComponent.maxHeight?'null':resizeComponent.maxHeight});}</script>
+</ss:equal>
+<ss:help/>
+</body>
+<script type="text/javascript">
+try{wd.display.showMsgPopup('${msg}');
+}catch(err){console.error(err);}
+</script>
+<ss:equal val="${empty wdclosewindowparam}" val2="false">
+<script type="text/javascript">
+try{wd.display.setCloseWindowParam('${wdclosewindowparam}');
+}catch(err){console.error(err);}
+</script>
+</ss:equal>
+
+</html>
+<%@ include file="/page/clip/footer.jsp" %>
+
+<script>
+	function getFormAppVm(){
+		var appEl = document.getElementById("app");
+		if (!appEl || !appEl.__vue_app__ || !appEl.__vue_app__._instance) {
+			return null;
+		}
+		return appEl.__vue_app__._instance.proxy || null;
+	}
+
+	function normalizeRylbmValues(value){
+		if (Array.isArray(value)) {
+			return value.map(function(item){
+				return item == null ? "" : item.toString();
+			}).filter(Boolean);
+		}
+		if (value == null || value === "") {
+			return [];
+		}
+		var cleanValue = value.toString().replace(/^,+/, "");
+		if (!cleanValue) {
+			return [];
+		}
+		return cleanValue.split(/[,|]/).filter(Boolean);
+	}
+
+	function hasRelativeRylbmValue(value){
+		return normalizeRylbmValues(value).indexOf("1000") !== -1;
+	}
+
+	function clearRyDisplay(){
+		var bmbjEl = document.getElementById('bmbj');
+		var xmEl = document.getElementById('xm');
+		var ryhEl = document.getElementById('ryh');
+		if (bmbjEl) bmbjEl.innerHTML = "";
+		if (xmEl) xmEl.innerHTML = "";
+		if (ryhEl) ryhEl.innerHTML = "";
+	}
+
+	function clearRySelection(vm){
+		if (!vm) {
+			clearRyDisplay();
+			return;
+		}
+		vm.ryid = "";
+		clearRyDisplay();
+	}
+
+	function handleRylbmChange(groupValue){
+		if (!hasRelativeRylbmValue(groupValue)) {
+			return;
+		}
+		var vm = getFormAppVm();
+		if (!vm) {
+			clearRyDisplay();
+			return;
+		}
+		vm.bmid = "";
+		clearRySelection(vm);
+	}
+
+	function handleBjChange(value){
+		if (value == null || value === "") {
+			return;
+		}
+		var vm = getFormAppVm();
+		if (!vm) {
+			clearRyDisplay();
+			return;
+		}
+		if (hasRelativeRylbmValue(vm.rylbm)) {
+			vm.rylbm = "1100";
+		}
+		clearRySelection(vm);
+	}
+	
+	function selBaseInfoByRyid(value){
+		$.ajax({
+			url:"<ss:serv name='ry_selBaseInfoByRyid'/>",
+			type:"post",
+			data:{
+				ryid:value
+			},
+			dataType:"json",
+			success:function(data){
+				if (data.ssCode != 0) {
+					alert(data.ssMsg);
+					return;
+				}
+				var d = data.ssData;
+				document.getElementById('xm').innerHTML = d.xm;
+				document.getElementById('ryh').innerHTML = d.ryh;
+				if (d.rylbm != 1100) {	// 不是学员。Lin
+					if (d.bmmc)
+						document.getElementById('bmbj').innerHTML = d.bmmc;
+					else
+						document.getElementById('bmbj').innerHTML = "(无)";
+				} else {
+					if (d.bjmc)
+						document.getElementById('bmbj').innerHTML = d.bjmc;
+					else
+						document.getElementById('bmbj').innerHTML = "(无)";
+				}
+				document.getElementById('xfye').innerHTML = d.xfye;
+			}
+		});
+	}
+</script>
+<script type="text/javascript" src="/js/validate/validator-rules.js"></script><%-- 功能说明:个人退费页接入桌面端校验规则,支持 SsInp 红线提示 by xu 20260323 --%>
+<script type="text/javascript" src="/js/validate/validation-manager.js"></script><%-- 功能说明:个人退费页接入桌面端校验管理器,支持 SsInp 红线提示 by xu 20260323 --%>
+
+<script>
+	var GRTF_LABEL_MAP = window.GRTF_LABEL_MAP || {};
+
+	// 功能说明:记录当前退费类别的最新 value/label,避免 onoff 隐藏值切换时机导致金额校验判断失真 by xu 20260323
+	var CURRENT_GRTF_CATEGORY_STATE = {
+		value: "",
+		label: ""
+	};
+
+	// 功能说明:退费类别变化后延后一拍重跑金额校验,确保隐藏字段值更新后再清理红线状态 by xu 20260323
+	function handleGrczlbmChange(groupValue, value, label){
+		CURRENT_GRTF_CATEGORY_STATE.value = groupValue == null ? "" : groupValue.toString();
+		CURRENT_GRTF_CATEGORY_STATE.label = label == null ? "" : label.toString();
+		setTimeout(function(){
+			if (window.ssVm && typeof window.ssVm.validateField === "function") {
+				window.ssVm.validateField("je");
+				return;
+			}
+			validateNegativeAmountByCategory(false);
+		}, 0);
+	}
+
+	// 功能说明:初始化个人退费页金额负数校验规则,整页金额都必须输入负数 by xu 20260323
+	function initNegativeAmountValidation(){
+		if (!document.querySelector('[name="je"]') || !document.querySelector('[name="grczlbm"]')) {
+			return;
+		}
+		if (!window.ssVm || typeof window.ssVm.add !== "function" || window.ss.dom._grczGrtfNegativeAmountValidationInited) {
+			return;
+		}
+		window.ss.dom._grczGrtfNegativeAmountValidationInited = true;
+		window.ssVm.add("ss.commonValidator.custom", ["je"], {
+			msgPrfx: "金额",
+			relField: "grczlbm",
+			validate: function(value, categoryValue){
+				var currentLabel = getCurrentGrczlbmLabel(categoryValue);
+				if (value == null || value.toString().trim() === "") {
+					return true;
+				}
+				if (isValidNegativeAmountText(value)) {
+					return true;
+				}
+				return {
+					valid: false,
+					message: (currentLabel || "退费") + "金额只能输入负数"
+				};
+			}
+		}, {
+			je: window.ss.dom.formElemConfig.je ? window.ss.dom.formElemConfig.je.val : ""
+		});
+	}
+
+	// 功能说明:根据当前退费类别值读取显示文案,供联动校验和错误提示复用 by xu 20260323
+	function getCurrentGrczlbmLabel(categoryValue){
+		var currentValue = categoryValue == null ? getCurrentGrczlbmValue() : categoryValue.toString();
+		if (CURRENT_GRTF_CATEGORY_STATE.label && (!currentValue || CURRENT_GRTF_CATEGORY_STATE.value === currentValue)) {
+			return CURRENT_GRTF_CATEGORY_STATE.label;
+		}
+		return GRTF_LABEL_MAP[currentValue] || "";
+	}
+
+	// 功能说明:统一判断金额文本是否为合法负数,供提交校验和 ssVm 规则复用 by xu 20260323
+	function isValidNegativeAmountText(value){
+		var amountText = value == null ? "" : value.toString().trim();
+		if (!amountText) {
+			return false;
+		}
+		var amountNumber = Number(amountText);
+		return !isNaN(amountNumber) && amountNumber < 0;
+	}
+
+	// 功能说明:根据当前退费类别值读取隐藏字段值,供金额校验复用 by xu 20260323
+	function getCurrentGrczlbmValue(){
+		var vm = getFormAppVm();
+		if (vm && vm.grczlbm != null && vm.grczlbm !== "") {
+			return vm.grczlbm.toString();
+		}
+		var categoryElem = document.querySelector('[name="grczlbm"]');
+		return categoryElem && categoryElem.value != null ? categoryElem.value.toString() : "";
+	}
+
+	// 功能说明:统一校验个人退费金额必须为负数,不再区分具体退费类别 by xu 20260323
+	function validateNegativeAmountByCategory(showMsg){
+		var currentLabel = getCurrentGrczlbmLabel();
+		var jeElem = document.querySelector('[name="je"]');
+		if (!jeElem) {
+			return true;
+		}
+
+		if (isValidNegativeAmountText(jeElem.value)) {
+			return true;
+		}
+
+		if (showMsg !== false) {
+			alert((currentLabel || "退费") + "金额只能输入负数");
+			jeElem.focus();
+		}
+		return false;
+	}
+
+	// 功能说明:个人退费页提交前先走 ssVm 全量校验,确保显示 SsInp 左侧红线与底部提示 by xu 20260323
+	function submitGrtfForm(){
+/* 去掉,不是 个人退费页 了。Lin
+		if (window.ssVm && window.ssVm.validations && window.ssVm.validations.size > 0) {
+			var validateResult = window.ssVm.validateAll();
+			if (!validateResult.valid) {
+				return false;
+			}
+		} else if (!validateNegativeAmountByCategory(true)) {
+			return false;
+		}
+*/
+
+		var formElem = document.querySelector("form");
+		if (!formElem) {
+			alert("表单不存在");
+			return false;
+		}
+
+		formElem.action = "<ss:serv name='ka_lr_tj' dest='addSure' parm='{thisViewObject:\"ka\",dataType:\"update\"}'/>";
+		ss.display.resizeComponent(881,361,515,515);
+		formElem.submit();
+		return true;
+	}
+
+	if (window.SS && typeof SS.ready === "function") {
+		SS.ready(function(){
+			// 功能说明:页面初始化后同步当前退费类别状态,避免首次校验读取不到最新类别文案 by xu 20260323
+			CURRENT_GRTF_CATEGORY_STATE.value = getCurrentGrczlbmValue();
+			CURRENT_GRTF_CATEGORY_STATE.label = GRTF_LABEL_MAP[CURRENT_GRTF_CATEGORY_STATE.value] || "";
+			// 功能说明:页面初始化后注册个人退费金额负数校验规则,保证 SsInp 输入时直接出现红线提示 by xu 20260323
+			initNegativeAmountValidation();
+		});
+	}
+
+</script>