index.vue 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168
  1. <template>
  2. <view class="page">
  3. <!-- 导航按钮 -->
  4. <button @click="logout" class="nav-btn">
  5. <text class="icon fa fa-sign-out"></text>
  6. </button>
  7. <button @click="toggleServerConfig" class="nav-btn">
  8. <text class="icon fa fa-cog"></text>
  9. </button>
  10. <scroll-view scroll-y class="scroll-area">
  11. <view v-if="!showServerConfig" class="form-section">
  12. <view class="section-title">
  13. <text class="title"></text>
  14. </view>
  15. <!-- RFID扫描区域 -->
  16. <view class="rfid-card">
  17. <text class="icon fa fa-qrcode scan-icon"></text>
  18. <view class="btn-group">
  19. <button class="manual-btn" @click="init" :disabled="isDisable">
  20. <text class="fa fa-power-off"></text> {{ isDeviceReady ? '已开启' : '开启设备' }}
  21. </button>
  22. <!-- <button class="btn" type="primary" @click="release" :disabled="isReDisabled">release</button> -->
  23. <button class="scan-btn" @click="scan" :disabled="!isDisable">
  24. <text class="fa fa-camera"></text> 扫描耳标签
  25. </button>
  26. </view>
  27. <input class="input-box" v-model="form.earId" disabled placeholder="当前FID标签号码将显示在这里" style="height: 30px;width: 95%;" />
  28. <!-- 存储的FID标签数据 -->
  29. <scroll-view scroll-y class="sv">
  30. <view v-for="(item, index) in dataList" :key="index" class="data-item">
  31. <p style="margin-left: 10px;">{{ item.id }}</p>
  32. <picker @change="onTypeChange($event, index)" :value="item.typeIndex" :range="types">
  33. <view class="picker">
  34. {{ types[item.typeIndex] }}
  35. </view>
  36. </picker>
  37. <button class="delete-btn" @click="deleteItem(index)">删除</button>
  38. </view>
  39. </scroll-view>
  40. </view>
  41. <!-- 操作按钮组 -->
  42. <view class="btn-group">
  43. <button class="manual-btn" @click="resetForm">重置</button>
  44. <button class="scan-btn" @click="submitForm">提交数据</button>
  45. </view>
  46. </view>
  47. </scroll-view>
  48. </view>
  49. </template>
  50. <script>
  51. // 导入API配置
  52. import API from '@/api/index.js'
  53. export default {
  54. data() {
  55. return {
  56. dataList: [], // 存储扫描的耳标数据
  57. types: ['正常', '淘汰', '死亡'], // 耳标类型选项
  58. scanProgress: 0, // 当前扫描进度
  59. scanTotalAttempts: 0, // 总扫描尝试次数
  60. isDisable: false, // 设备禁用状态
  61. isDeviceReady: false, // 设备就绪状态
  62. isInitializing: false, // 设备初始化中状态
  63. currentDate: '', // 当前日期
  64. showServerConfig: false,// 是否显示服务器配置
  65. uhfSFHelper: null, // UHF插件实例
  66. scanTimeout: null, // 扫描超时计时器
  67. retryTimeout: null, // 重试计时器
  68. maxScanTimer: null, // 最大扫描时间计时器
  69. settingChangeListener: null, // 设置变化监听器
  70. // 列表数据
  71. buildingList: [], // 栋舍列表
  72. roomList: [], // 房间列表
  73. Fieldnumber: [], // 栏位列表
  74. // 表单数据
  75. form: {
  76. earId: '', // 耳标ID
  77. buildingName: '', // 栋舍名称
  78. roomName: '', // 房间名称
  79. penNo: '', // 栏位编号
  80. status: 'healthy', // 状态
  81. note: '' // 备注
  82. },
  83. // 提交状态
  84. isSubmitting: false,
  85. }
  86. },
  87. mounted() {
  88. // 标记组件已挂载
  89. this._isMounted = true;
  90. // 初始化日期
  91. const now = new Date()
  92. this.currentDate = now.toISOString().split('T')[0]
  93. // 加载已保存的设置
  94. this.loadSavedSettings();
  95. // 获取列表数据(即使插件未初始化也可以加载)
  96. this.fetchBuildingList();
  97. // 监听登录成功事件,重新加载用户设置并清空数据
  98. this.reloadUserSettingsListener = uni.$on('reloadUserSettings', () => {
  99. // 清空数据
  100. this.resetForm();
  101. this.loadSavedSettings();
  102. // 重新加载栋舍列表,确保使用最新的用户信息
  103. this.fetchBuildingList();
  104. });
  105. // 初始化插件实例
  106. this.initializePluginWithRetry(0);
  107. // 监听全局设置变化
  108. this.settingChangeListener = uni.$on('settingsUpdated', (settings) => {
  109. this.loadSavedSettings(settings);
  110. });
  111. },
  112. /**
  113. * 带重试的插件初始化
  114. * @param {number} retryCount - 当前重试次数
  115. */
  116. initializePluginWithRetry(retryCount = 0) {
  117. console.log('设备初始化成功');
  118. this.isDisable = false;
  119. uni.showToast({
  120. title: '设备初始化成功',
  121. icon: 'success',
  122. duration: 2000
  123. });
  124. //如果开启设备 初始化失败,重试,请打开下面代码,上方代码可以删除掉
  125. const MAX_RETRIES = 1;
  126. const RETRY_DELAY = 2000; // 2秒
  127. const isPluginInitialized = this.initPluginInstance();
  128. // if (!isPluginInitialized) {
  129. // console.warn(`Plugin initialization failed (attempt ${retryCount + 1}/${MAX_RETRIES})`);
  130. // this.isDisable = true; // 禁用依赖插件的按钮
  131. // // 如果未达到最大重试次数,继续重试
  132. // if (retryCount < MAX_RETRIES) {
  133. // console.log(`Retrying plugin initialization after ${RETRY_DELAY}ms`);
  134. // setTimeout(() => {
  135. // if (this._isMounted) {
  136. // this.initializePluginWithRetry(retryCount + 1);
  137. // }
  138. // }, RETRY_DELAY);
  139. // } else {
  140. // console.error('Max retries reached for plugin initialization');
  141. // uni.showToast({
  142. // title: '设备初始化失败,请重启应用',
  143. // icon: 'none',
  144. // duration: 3000
  145. // });
  146. // }
  147. // } else {
  148. // console.log('Plugin initialized successfully');
  149. // this.isDisable = false;
  150. // uni.showToast({
  151. // title: '设备初始化成功',
  152. // icon: 'success',
  153. // duration: 2000
  154. // });
  155. // }
  156. },
  157. beforeUnmount() {
  158. // 标记组件已卸载
  159. this._isMounted = false;
  160. // 清除所有计时器
  161. this.cancelScan();
  162. // 释放设备
  163. this.releaseDevice();
  164. // 清理插件实例
  165. this.uhfSFHelper = null;
  166. // 移除事件监听
  167. if (this.settingChangeListener) {
  168. uni.$off('settingsUpdated', this.settingChangeListener);
  169. }
  170. // 移除登录成功事件监听
  171. if (this.reloadUserSettingsListener) {
  172. uni.$off('reloadUserSettings', this.reloadUserSettingsListener);
  173. }
  174. },
  175. /**
  176. * 页面显示时触发,确保获取最新的编号信息
  177. */
  178. onShow() {
  179. this.loadSavedSettings();
  180. },
  181. methods: {
  182. /**
  183. * 带重试的插件初始化
  184. * @param {number} retryCount - 当前重试次数
  185. */
  186. initializePluginWithRetry(retryCount = 0) {
  187. const MAX_RETRIES = 1;
  188. const RETRY_DELAY = 2000; // 2秒
  189. const isPluginInitialized = this.initPluginInstance();
  190. if (!isPluginInitialized) {
  191. console.warn(`Plugin initialization failed (attempt ${retryCount + 1}/${MAX_RETRIES})`);
  192. this.isDisable = true; // 禁用依赖插件的按钮
  193. // 如果未达到最大重试次数,继续重试
  194. if (retryCount < MAX_RETRIES) {
  195. console.log(`Retrying plugin initialization after ${RETRY_DELAY}ms`);
  196. setTimeout(() => {
  197. if (this._isMounted) {
  198. this.initializePluginWithRetry(retryCount + 1);
  199. }
  200. }, RETRY_DELAY);
  201. } else {
  202. console.error('Max retries reached for plugin initialization');
  203. uni.showToast({
  204. title: '设备初始化失败,请重启应用',
  205. icon: 'none',
  206. duration: 3000
  207. });
  208. }
  209. } else {
  210. console.log('插件初始化成功');
  211. this.isDisable = false;
  212. uni.showToast({
  213. title: '设备初始化成功',
  214. icon: 'success',
  215. duration: 2000
  216. });
  217. }
  218. },
  219. /**
  220. * 获取栋舍列表(带重试机制)
  221. * @param {number} retryCount - 当前重试次数(默认0)
  222. */
  223. fetchBuildingList(retryCount = 0) {
  224. // 显示加载提示
  225. if (retryCount === 0) {
  226. uni.showLoading({ title: '加载栋舍列表...', mask: true });
  227. }
  228. // 发送请求到API获取栋舍列表
  229. uni.request({
  230. url: API.getBuilding,
  231. method: 'GET',
  232. timeout: 10000, // 增加超时时间到10秒
  233. success: (res) => {
  234. // 隐藏加载提示
  235. if (retryCount === 0) {
  236. uni.hideLoading();
  237. }
  238. // 更新栋舍列表
  239. this.buildingList = res.data.data || [];
  240. }
  241. });
  242. },
  243. /**
  244. * 加载已保存的设置
  245. * @param {object} settings - 可选的设置参数
  246. */
  247. loadSavedSettings(settings = null) {
  248. const app = getApp();
  249. let buildingName, roomName, penNo;
  250. // 如果提供了设置参数,优先使用参数值
  251. if (settings && typeof settings === 'object') {
  252. buildingName = settings.building || '';
  253. roomName = settings.room || '';
  254. penNo = settings.pen || '';
  255. // 更新全局数据
  256. app.globalData.buildingName = buildingName;
  257. app.globalData.roomName = roomName;
  258. app.globalData.penNo = penNo;
  259. // 更新本地存储
  260. uni.setStorageSync('buildingName', buildingName);
  261. uni.setStorageSync('roomName', roomName);
  262. uni.setStorageSync('penNo', penNo);
  263. } else {
  264. // 从全局数据或本地存储获取编号信息
  265. buildingName = app.globalData.buildingName || uni.getStorageSync('buildingName') || '';
  266. roomName = app.globalData.roomName || uni.getStorageSync('roomName') || '';
  267. penNo = app.globalData.penNo || uni.getStorageSync('penNo') || '';
  268. }
  269. // 更新表单数据
  270. this.form.buildingName = buildingName;
  271. this.form.roomName = roomName;
  272. this.form.penNo = penNo;
  273. // 如果栋舍不为空且房间列表为空,加载房间列表
  274. if (buildingName && this.roomList.length === 0) {
  275. this.fetchRoomList(buildingName);
  276. } else if (buildingName && this.roomList.length > 0 && roomName) {
  277. // 如果房间已变更,更新房间列表
  278. this.fetchRoomList(buildingName);
  279. }
  280. // 如果房间不为空且栏位列表为空,加载栏位列表
  281. if (roomName && this.Fieldnumber.length === 0) {
  282. this.fetchFieldList(roomName);
  283. } else if (roomName && this.Fieldnumber.length > 0 && penNo) {
  284. // 如果栏位已变更,更新栏位列表
  285. this.fetchFieldList(roomName);
  286. }
  287. },
  288. /**
  289. * 根据栋舍获取房间列表(带重试机制)
  290. * @param {string} buildingName - 栋舍名称
  291. * @param {number} retryCount - 当前重试次数
  292. */
  293. fetchRoomList(buildingName, retryCount = 0) {
  294. // 显示加载提示
  295. if (retryCount === 0) {
  296. uni.showLoading({ title: '加载房间列表...', mask: true });
  297. }
  298. // 发送请求到API获取房间列表
  299. uni.request({
  300. url: API.getRoom,
  301. method: 'GET',
  302. data: { building: buildingName },
  303. timeout: 10000, // 增加超时时间到10秒
  304. success: (res) => {
  305. // 隐藏加载提示
  306. if (retryCount === 0) {
  307. uni.hideLoading();
  308. }
  309. // 更新房间列表
  310. this.roomList = res.data.data || [];
  311. // 重置房间和栏位选择
  312. this.form.roomName = '';
  313. this.Fieldnumber = [];
  314. this.form.penNo = '';
  315. }
  316. });
  317. },
  318. /**
  319. * 根据房间名称获取栏位列表(带重试机制)
  320. * @param {string} roomName - 房间名称
  321. * @param {number} retryCount - 当前重试次数(默认0)
  322. */
  323. fetchFieldList(roomName, retryCount = 0) {
  324. // 显示加载提示
  325. if (retryCount === 0) {
  326. uni.showLoading({ title: '加载栏位列表...', mask: true });
  327. }
  328. const MAX_RETRIES = 1;
  329. const RETRY_DELAY = 2000; // 2秒
  330. // 发送请求到API获取栏位列表
  331. uni.request({
  332. url: API.getField,
  333. method: 'GET',
  334. data: { room: roomName },
  335. timeout: 10000, // 增加超时时间到10秒
  336. success: (res) => {
  337. // 隐藏加载提示
  338. if (retryCount === 0) {
  339. uni.hideLoading();
  340. }
  341. // 更新栏位列表
  342. this.Fieldnumber = res.data.data || [];
  343. // 重置栏位选择
  344. this.form.penNo = '';
  345. },
  346. fail: (err) => {
  347. // 隐藏加载提示
  348. if (retryCount === 0) {
  349. uni.hideLoading();
  350. }
  351. // 如果未达到最大重试次数,尝试重试
  352. if (retryCount < MAX_RETRIES) {
  353. console.log(`Retrying request (${retryCount + 1}/${MAX_RETRIES})`);
  354. setTimeout(() => {
  355. this.fetchFieldList(roomName, retryCount + 1);
  356. }, RETRY_DELAY);
  357. } else {
  358. }
  359. }
  360. });
  361. },
  362. /**
  363. * 初始化UHF插件实例
  364. * @returns {boolean} 初始化是否成功
  365. */
  366. initPluginInstance() {
  367. if (this.uhfSFHelper) {
  368. console.log('插件实例已存在');
  369. return true;
  370. }
  371. try {
  372. console.log('初始化UHF插件实例');
  373. // 检查运行环境是否支持原生插件
  374. if (typeof uni.requireNativePlugin !== 'function') {
  375. console.error('当前环境不支持uni.requireNativePlugin');
  376. return false;
  377. }
  378. // 尝试加载插件
  379. this.uhfSFHelper = uni.requireNativePlugin('Alvin-CBZUhfModule');
  380. // 验证插件实例是否有效
  381. if (!this.uhfSFHelper) {
  382. console.error('插件实例为空或未定义');
  383. // 检查插件是否存在于项目中
  384. console.log('检查插件是否存在于项目中...');
  385. // 提示可能的解决方案
  386. console.warn('可能的解决方案:');
  387. console.warn('1. 验证插件名称是否正确:Alvin-CBZUhfModule');
  388. console.warn('2. 检查插件是否正确安装在nativeplugins目录中');
  389. console.warn('3. 确保您的开发环境支持原生插件');
  390. return false;
  391. }
  392. if (typeof this.uhfSFHelper.doInitDevice !== 'function') {
  393. console.error('插件实例缺少必要方法: doInitDevice');
  394. // 输出插件实例的所有方法,帮助调试
  395. console.log('插件实例可用方法:', Object.keys(this.uhfSFHelper));
  396. this.uhfSFHelper = null;
  397. return false;
  398. }
  399. console.log('UHF插件实例创建成功');
  400. return true;
  401. } catch (e) {
  402. console.error('加载UHF插件失败:', e.message);
  403. console.error('错误栈:', e.stack);
  404. this.uhfSFHelper = null;
  405. uni.showToast({
  406. title: '设备功能不可用: ' + e.message,
  407. icon: 'none',
  408. duration: 3000
  409. });
  410. return false;
  411. }
  412. },
  413. /**
  414. * 检查并恢复插件实例
  415. * @param {number} retryCount - 当前重试次数(默认0)
  416. * @returns {boolean} 插件实例是否有效
  417. */
  418. checkAndRestorePluginInstance(retryCount = 0) {
  419. // 设置最大重试次数和超时时间
  420. const MAX_RETRIES = 1;
  421. const RETRY_DELAY = 1000; // 1秒
  422. // 如果实例不存在,尝试初始化
  423. if (!this.uhfSFHelper) {
  424. console.log(`没有插件实例,尝试初始化 (重试: ${retryCount})`);
  425. const result = this.initPluginInstance();
  426. // 如果初始化失败且未达到最大重试次数,递归重试
  427. if (!result && retryCount < MAX_RETRIES) {
  428. console.log(`初始化失败,重试 (${retryCount + 1}/${MAX_RETRIES}) 后 ${RETRY_DELAY}ms`);
  429. // 延迟后重试
  430. setTimeout(() => {
  431. this.checkAndRestorePluginInstance(retryCount + 1);
  432. }, RETRY_DELAY);
  433. return false;
  434. }
  435. return result;
  436. }
  437. // 检查实例方法是否存在且有效
  438. const requiredMethods = ['doInitDevice', 'doStartScan', 'doReleaseDevice'];
  439. const missingMethods = requiredMethods.filter(method => typeof this.uhfSFHelper[method] !== 'function');
  440. if (missingMethods.length > 0) {
  441. console.error(`插件实例缺少必要方法: ${missingMethods.join(', ')}`);
  442. console.log('插件实例可用方法:', Object.keys(this.uhfSFHelper));
  443. this.uhfSFHelper = null;
  444. // 如果未达到最大重试次数,尝试重新初始化
  445. if (retryCount < MAX_RETRIES) {
  446. console.log(`Attempting to reinitialize plugin (${retryCount + 1}/${MAX_RETRIES}) after ${RETRY_DELAY}ms`);
  447. setTimeout(() => {
  448. this.checkAndRestorePluginInstance(retryCount + 1);
  449. }, RETRY_DELAY);
  450. return false;
  451. }
  452. return false;
  453. }
  454. console.log('插件实例有效且准备使用');
  455. return true;
  456. },
  457. /**
  458. * 取消当前扫描操作
  459. */
  460. cancelScan() {
  461. // 清除所有相关计时器
  462. if (this.scanTimeout) {
  463. clearTimeout(this.scanTimeout);
  464. this.scanTimeout = null;
  465. }
  466. if (this.retryTimeout) {
  467. clearTimeout(this.retryTimeout);
  468. this.retryTimeout = null;
  469. }
  470. if (this.maxScanTimer) {
  471. clearTimeout(this.maxScanTimer);
  472. this.maxScanTimer = null;
  473. }
  474. // 隐藏加载提示
  475. uni.hideLoading();
  476. console.log('扫描已取消');
  477. uni.showToast({ title: '扫描已取消', icon: 'none' });
  478. },
  479. /**
  480. * 释放设备资源
  481. */
  482. releaseDevice() {
  483. if (this.isDeviceReady && this.uhfSFHelper) {
  484. try {
  485. this.uhfSFHelper.doReleaseDevice()
  486. } catch (e) {
  487. console.error('释放设备失败', e)
  488. }
  489. this.isDeviceReady = false
  490. this.isDisable = false
  491. }
  492. },
  493. /**
  494. * 初始化设备
  495. */
  496. init() {
  497. // 确保插件实例已初始化且有效
  498. if (!this.checkAndRestorePluginInstance()) {
  499. return;
  500. }
  501. if (this.isInitializing) {
  502. console.log('设备初始化已在进行中');
  503. return;
  504. }
  505. if (this.isDeviceReady) {
  506. console.log('设备已初始化');
  507. return uni.showToast({ title: '设备已开启', icon: 'none' });
  508. }
  509. this.isDisable = false;
  510. this.isInitializing = true;
  511. try {
  512. console.log('开始初始化设备');
  513. // 再次检查插件实例是否有效
  514. if (!this.checkAndRestorePluginInstance()) {
  515. this.isInitializing = false;
  516. return;
  517. }
  518. this.uhfSFHelper.doInitDevice(res => {
  519. this.isInitializing = false;
  520. if (res === true) {
  521. this.isDeviceReady = true;
  522. this.isDisable = true;
  523. console.log('Device initialized successfully');
  524. uni.showToast({ title: '设备已开启', icon: 'success' });
  525. } else {
  526. this.isDisable = false;
  527. console.error('设备初始化失败');
  528. uni.showToast({ title: '初始化失败', icon: 'none' });
  529. }
  530. });
  531. } catch (e) {
  532. this.isInitializing = false;
  533. console.error('Error during device initialization:', e);
  534. this.isDisable = false;
  535. // 清除插件实例,以便下次初始化尝试
  536. this.uhfSFHelper = null;
  537. uni.showToast({ title: '初始化异常', icon: 'none' });
  538. }
  539. },
  540. /**
  541. * 开始扫描耳标
  542. */
  543. scan() {
  544. // 确保插件实例已初始化且有效
  545. if (!this.checkAndRestorePluginInstance()) {
  546. return;
  547. }
  548. if (!this.isDeviceReady) {
  549. return uni.showToast({ title: '请先开启设备', icon: 'none' })
  550. }
  551. // 设置扫描参数 - 优化参数以提高成功率
  552. const scanConfig = {
  553. retryCount: 3, // 增加重试次数
  554. currentRetry: 0,
  555. timeout: 2000, // 增加超时时间
  556. interval: 400, // 增加间隔,给设备恢复时间
  557. signalThreshold: 0.5 // 降低信号阈值,接受更多信号
  558. };
  559. // 初始化扫描进度变量
  560. this.scanProgress = 0;
  561. this.scanTotalAttempts = scanConfig.retryCount;
  562. // 显示扫描中提示,添加mask以禁止背景操作
  563. uni.showLoading({
  564. title: '正在扫描耳标...',
  565. mask: true
  566. });
  567. // 执行扫描函数
  568. this.performScan(scanConfig);
  569. },
  570. // release() {
  571. // this.isDeviceReady = false;
  572. // this.isDisable = false;
  573. // },
  574. /**
  575. * 执行扫描(带重试机制)
  576. * @param {object} config - 扫描配置参数
  577. * @param {number} config.retryCount - 最大重试次数
  578. * @param {number} config.currentRetry - 当前重试次数
  579. * @param {number} config.timeout - 单次扫描超时时间(毫秒)
  580. * @param {number} config.interval - 重试间隔(毫秒)
  581. */
  582. /**
  583. * 处理类型选择变化
  584. */
  585. onTypeChange(e, index) {
  586. if (index >= 0 && index < this.dataList.length) {
  587. this.dataList[index].typeIndex = e.detail.value;
  588. console.log(`耳标 ${this.dataList[index].id} 类型变更为: ${this.types[e.detail.value]}`);
  589. }
  590. },
  591. /**
  592. * 删除耳标条目
  593. */
  594. deleteItem(index) {
  595. if (index >= 0 && index < this.dataList.length) {
  596. const deletedItem = this.dataList.splice(index, 1);
  597. console.log(`删除耳标: ${deletedItem[0].id}`);
  598. uni.showToast({ title: '删除成功', icon: 'success' });
  599. }
  600. },
  601. performScan(config) {
  602. // 检查组件是否已卸载或设备是否已准备好
  603. if (!this._isMounted || !this.isDeviceReady) {
  604. uni.hideLoading();
  605. console.log('Scan aborted: component not mounted or device not ready');
  606. return;
  607. }
  608. if (config.currentRetry >= config.retryCount) {
  609. uni.hideLoading();
  610. console.log('Scan failed after maximum retries');
  611. return uni.showToast({ title: '扫描失败,请调整位置重试', icon: 'none' });
  612. }
  613. // 添加最大扫描时间限制 (总时间 = 超时时间 * 重试次数)
  614. const maxScanTime = config.timeout * config.retryCount;
  615. if (!this.maxScanTimer) {
  616. this.maxScanTimer = setTimeout(() => {
  617. console.log('Maximum scan time exceeded');
  618. this.cancelScan();
  619. }, maxScanTime);
  620. }
  621. // 增加重试计数
  622. config.currentRetry++;
  623. this.scanProgress = config.currentRetry;
  624. // 更新加载提示(简化提示,避免频繁更新)
  625. if (config.currentRetry % 2 === 0 || config.currentRetry === config.retryCount) {
  626. uni.hideLoading();
  627. uni.showLoading({
  628. title: `扫描中 (${Math.round(config.currentRetry/config.retryCount*100)}%)...`,
  629. mask: true
  630. });
  631. }
  632. console.log(`开始扫描尝试 ${config.currentRetry}/${config.retryCount},超时时间: ${config.timeout}ms`);
  633. // 清除之前的超时
  634. if (this.scanTimeout) {
  635. clearTimeout(this.scanTimeout);
  636. this.scanTimeout = null;
  637. }
  638. // 设置超时机制
  639. this.scanTimeout = setTimeout(() => {
  640. console.log(`Scan timeout, retrying (${config.currentRetry}/${config.retryCount})`);
  641. // 重试扫描前先检查实例
  642. if (this._isMounted) {
  643. // 检查并恢复插件实例
  644. if (this.checkAndRestorePluginInstance()) {
  645. this.retryTimeout = setTimeout(() => this.performScan(config), config.interval);
  646. } else {
  647. console.error('Failed to restore plugin instance before retry');
  648. uni.hideLoading();
  649. return uni.showToast({ title: '设备功能异常,无法重试', icon: 'none' });
  650. }
  651. }
  652. }, config.timeout);
  653. // 执行扫描
  654. try {
  655. // 清除之前可能存在的重试计时器
  656. if (this.retryTimeout) {
  657. clearTimeout(this.retryTimeout);
  658. this.retryTimeout = null;
  659. }
  660. // 检查插件实例是否有效
  661. if (!this.checkAndRestorePluginInstance()) {
  662. // 清除超时计时器
  663. if (this.scanTimeout) {
  664. clearTimeout(this.scanTimeout);
  665. this.scanTimeout = null;
  666. }
  667. console.error('Invalid plugin instance for scanning after restore attempt');
  668. // 尝试重新初始化设备
  669. this.initDevice();
  670. uni.hideLoading();
  671. return uni.showToast({ title: '设备功能异常,正在重新初始化', icon: 'none' });
  672. }
  673. // 保存当前实例引用,防止闭包中实例变化
  674. const currentPluginInstance = this.uhfSFHelper;
  675. // 执行扫描命令
  676. try {
  677. currentPluginInstance.doStartScan(result => {
  678. // 再次检查插件实例是否与执行扫描时相同
  679. if (this.uhfSFHelper !== currentPluginInstance) {
  680. console.warn('Plugin instance changed during scan, ignoring result');
  681. // 尝试重新扫描
  682. if (this._isMounted) {
  683. this.retryTimeout = setTimeout(() => this.performScan(config), config.interval);
  684. }
  685. return;
  686. }
  687. // 检查组件是否已卸载
  688. if (!this._isMounted) return;
  689. // 清除超时计时器
  690. if (this.scanTimeout) {
  691. clearTimeout(this.scanTimeout);
  692. this.scanTimeout = null;
  693. }
  694. if (result) {
  695. // 假设result是一个包含id和signalStrength的对象
  696. // 如果result只是ID字符串,我们将其包装成对象
  697. const scanResult = typeof result === 'string' ? { id: result, signalStrength: 1 } : result;
  698. // 检查信号强度是否足够
  699. if (scanResult.signalStrength >= config.signalThreshold) {
  700. // 清除所有计时器
  701. if (this.maxScanTimer) {
  702. clearTimeout(this.maxScanTimer);
  703. this.maxScanTimer = null;
  704. }
  705. if (this.retryTimeout) {
  706. clearTimeout(this.retryTimeout);
  707. this.retryTimeout = null;
  708. }
  709. uni.hideLoading();
  710. console.log('扫描成功:', scanResult);
  711. this.form.earId = scanResult.id;
  712. // 检查是否重复扫描
  713. const isDuplicate = this.dataList.some(item => item.id === scanResult.id);
  714. if (isDuplicate) {
  715. console.log('耳标已存在:', scanResult.id);
  716. uni.showToast({ title: '该耳标已扫描过', icon: 'none' });
  717. } else {
  718. // 默认添加为'正常'类型
  719. this.dataList.push({ id: scanResult.id, typeIndex: 0 });
  720. uni.showToast({ title: '扫描成功', icon: 'success' });
  721. }
  722. } else {
  723. console.log(`Scan result with weak signal (${scanResult.signalStrength}), retrying`);
  724. // 信号太弱,继续重试前检查实例
  725. if (this._isMounted) {
  726. if (this.checkAndRestorePluginInstance()) {
  727. this.retryTimeout = setTimeout(() => this.performScan(config), config.interval);
  728. } else {
  729. console.error('Failed to restore plugin instance for retry');
  730. uni.hideLoading();
  731. return uni.showToast({ title: '设备功能异常', icon: 'none' });
  732. }
  733. }
  734. }
  735. } else {
  736. console.log(`Scan failed, retrying (${config.currentRetry}/${config.retryCount})`);
  737. // 重试扫描前检查实例
  738. if (this._isMounted) {
  739. if (this.checkAndRestorePluginInstance()) {
  740. this.retryTimeout = setTimeout(() => this.performScan(config), config.interval);
  741. } else {
  742. console.error('Failed to restore plugin instance for retry');
  743. uni.hideLoading();
  744. return uni.showToast({ title: '设备功能异常', icon: 'none' });
  745. }
  746. }
  747. }
  748. });
  749. } catch (e) {
  750. console.error('Exception during scan execution:', e);
  751. // 清除超时计时器
  752. if (this.scanTimeout) {
  753. clearTimeout(this.scanTimeout);
  754. this.scanTimeout = null;
  755. }
  756. // 检查是否是实例不可用错误
  757. if (e.message && e.message.includes('instance is not available')) {
  758. console.error('Plugin instance not available during scan');
  759. // 尝试重新初始化
  760. this.uhfSFHelper = null;
  761. if (this.checkAndRestorePluginInstance()) {
  762. // 重新执行扫描
  763. if (this._isMounted) {
  764. this.retryTimeout = setTimeout(() => this.performScan(config), config.interval * 2);
  765. }
  766. } else {
  767. uni.hideLoading();
  768. return uni.showToast({ title: '设备功能异常,无法扫描', icon: 'none' });
  769. }
  770. } else {
  771. // 其他错误
  772. if (this._isMounted) {
  773. this.retryTimeout = setTimeout(() => this.performScan(config), config.interval);
  774. }
  775. }
  776. }
  777. } catch (e) {
  778. // 清除超时计时器
  779. if (this.scanTimeout) {
  780. clearTimeout(this.scanTimeout);
  781. this.scanTimeout = null;
  782. }
  783. console.error('Error during scan setup:', e);
  784. // 检查错误是否与实例不可用相关
  785. if (e.message && e.message.includes('instance is not available')) {
  786. console.error('Plugin instance not available during scan setup');
  787. // 清除当前实例
  788. this.uhfSFHelper = null;
  789. // 尝试重新初始化
  790. if (this.checkAndRestorePluginInstance()) {
  791. // 重新执行扫描
  792. if (this._isMounted) {
  793. this.retryTimeout = setTimeout(() => this.performScan(config), config.interval * 2); // 增加间隔
  794. }
  795. } else {
  796. uni.hideLoading();
  797. return uni.showToast({ title: '设备功能异常', icon: 'none' });
  798. }
  799. } else {
  800. // 其他错误,继续重试
  801. if (this._isMounted) {
  802. this.retryTimeout = setTimeout(() => this.performScan(config), config.interval);
  803. }
  804. }
  805. }
  806. },
  807. /**
  808. * 提交表单数据
  809. */
  810. submitForm() {
  811. // 确保加载最新的设置
  812. this.loadSavedSettings();
  813. // 验证编号是否已选择
  814. let missingField = '';
  815. if (!this.form.buildingName) missingField = '栋舍';
  816. else if (!this.form.roomName) missingField = '房间';
  817. else if (!this.form.penNo) missingField = '栏位';
  818. if (missingField) {
  819. return uni.showToast({ title: `未选择${missingField}编号`, icon: 'none', duration: 3000 })
  820. }
  821. // 验证耳标是否已扫描
  822. if (this.dataList.length === 0) {
  823. return uni.showToast({ title: '请先扫描耳标', icon: 'none', duration: 3000 })
  824. }
  825. // 验证用户ID是否存在
  826. const app = getApp();
  827. const userInfo = app.globalData.userInfo || uni.getStorageSync('user_info') || {};
  828. const userId = userInfo.id || '';
  829. if (!userId) {
  830. return uni.showToast({ title: '用户未登录', icon: 'none', duration: 3000 })
  831. }
  832. // 显示加载提示
  833. uni.showLoading({ title: '提交中...', mask: true });
  834. // 将dataList数组转换为包含类型的逗号分隔字符串
  835. // 格式: id:type,id:type,...
  836. const rfidString = this.dataList.map(item => {
  837. return `${item.id}:${this.types[item.typeIndex]}`;
  838. }).join(',');
  839. // 准备提交数据
  840. const submitData = {
  841. rfid: rfidString,
  842. buildingName: this.form.buildingName,
  843. roomName: this.form.roomName,
  844. penNo: this.form.penNo,
  845. userId: userId,
  846. username: userInfo.username || '',
  847. time: new Date().toISOString()
  848. };
  849. console.log('准备提交的数据:', submitData);
  850. // 发送请求到API
  851. this.submitData(submitData);
  852. },
  853. /**
  854. * 提交数据
  855. * @param {object} data - 要提交的数据
  856. */
  857. submitData(data) {
  858. // 设置独立的提交加载状态标志
  859. this.isSubmitting = true;
  860. uni.request({
  861. url: API.postListAdd,
  862. method: 'POST',
  863. data: data,
  864. timeout: 10000, // 设置10秒超时
  865. method: 'POST',
  866. data: data,
  867. timeout: 10000, // 设置10秒超时
  868. success: (res) => {
  869. console.log('API响应:', res);
  870. // 先隐藏加载提示
  871. if (this.isSubmitting) {
  872. uni.hideLoading();
  873. this.isSubmitting = false;
  874. }
  875. if (res.data) {
  876. console.log('服务器返回数据:', res.data);
  877. // 显示结果提示
  878. const message = res.data.msg || '提交成功';
  879. console.log('显示提示:', message);
  880. uni.showToast({
  881. title: message,
  882. icon: res.data.code === 0 ? 'success' : 'none',
  883. duration: 3000
  884. });
  885. if (res.data.code === 0) {
  886. // 添加本地记录
  887. this.records.unshift({
  888. rfid: data.rfid,
  889. building: data.buildingName,
  890. roomName: data.roomName,
  891. pen: data.penNo,
  892. userId: data.userId,
  893. time: new Date().toLocaleTimeString()
  894. });
  895. this.resetForm();
  896. }
  897. } else {
  898. console.error('提交失败: 响应数据格式不正确', res);
  899. // 显示错误提示
  900. uni.showToast({
  901. title: '提交失败,请重试',
  902. icon: 'none',
  903. duration: 3000
  904. });
  905. }
  906. },
  907. fail: (err) => {
  908. console.error('网络请求失败:', err);
  909. // 隐藏加载提示并显示错误提示
  910. if (this.isSubmitting) {
  911. uni.hideLoading();
  912. this.isSubmitting = false;
  913. }
  914. uni.showToast({
  915. title: '网络请求失败,请重试',
  916. icon: 'none',
  917. duration: 3000
  918. });
  919. // 接口失败不影响耳标扫描,无需额外处理
  920. },
  921. complete: () => {
  922. // 确保加载提示被隐藏
  923. setTimeout(() => {
  924. if (this.isSubmitting) {
  925. uni.hideLoading();
  926. this.isSubmitting = false;
  927. }
  928. }, 2000);
  929. }
  930. });
  931. },
  932. // 重置
  933. resetForm() {
  934. this.form.earId = ''
  935. this.form.buildingName = ''
  936. this.form.roomName = ''
  937. this.form.penNo = ''
  938. this.form.status = 'healthy'
  939. this.form.note = ''
  940. // 清除扫描结果列表
  941. this.dataList = []
  942. },
  943. // 选择
  944. onBuildingChange(e) {
  945. const buildingName = this.buildingList[e.detail.value];
  946. this.form.buildingName = buildingName;
  947. // 根据选择的栋舍加载房间列表
  948. if (buildingName) {
  949. this.fetchRoomList(buildingName);
  950. } else {
  951. // 清空房间和栏位列表
  952. this.roomList = [];
  953. this.Fieldnumber = [];
  954. this.form.roomName = '';
  955. this.form.penNo = '';
  956. }
  957. },
  958. onRoomChange(e) {
  959. const roomName = this.roomList[e.detail.value];
  960. this.form.roomName = roomName;
  961. // 根据选择的房间加载栏位列表
  962. if (roomName) {
  963. this.fetchFieldList(roomName);
  964. } else {
  965. // 清空栏位列表
  966. this.Fieldnumber = [];
  967. this.form.penNo = '';
  968. }
  969. },
  970. onFieldChange(e) {
  971. this.form.penNo = this.Fieldnumber[e.detail.value]
  972. },
  973. // 其他
  974. logout() {
  975. // 清空数据
  976. this.resetForm();
  977. // 释放设备资源
  978. this.releaseDevice();
  979. // 清除用户信息
  980. const app = getApp();
  981. app.globalData.userInfo = null;
  982. uni.removeStorageSync('user_info');
  983. // 跳转到登录页面
  984. uni.redirectTo({ url: '/pages/login/login' });
  985. uni.showToast({ title: '已退出登录', icon: 'none' })
  986. },
  987. toggleServerConfig() {
  988. this.showServerConfig = !this.showServerConfig
  989. }
  990. }
  991. }
  992. </script>
  993. <style>
  994. .page { display: flex; flex-direction: column; height: 100vh; width: 100%; box-sizing: border-box; padding: 0 10rpx; }
  995. .data-item { display: flex; justify-content: space-between; align-items: center; padding: 10rpx; margin-bottom: 10rpx; background-color: #f5f5f5; border-radius: 5rpx; }
  996. .picker { padding: 5rpx 10rpx;
  997. margin-left: 20px;
  998. background-color: #fff; border: 1px solid #ddd; border-radius: 5rpx; min-width: 120rpx; text-align: center; }
  999. .arrow::after { content: '▾'; font-size: 12rpx; margin-left: 5rpx; }
  1000. .delete-btn { background-color: #ff4d4f; color: white; font-size: 24rpx; padding: 5rpx 10rpx; min-width: 80rpx; line-height: normal; }
  1001. .nav-bar {
  1002. display: flex; justify-content: space-between; align-items: center;
  1003. background: #007aff; color: white; padding: 15rpx 30rpx;
  1004. }
  1005. .nav-title { font-weight: bold; font-size: 32rpx; }
  1006. .nav-btn { background: transparent; border: none; font-size: 32rpx; }
  1007. .scroll-area { flex: 1; }
  1008. .section-title { display: flex; justify-content: space-between; margin-bottom: 20rpx; }
  1009. .section-title .title { font-weight: bold; font-size: 30rpx; }
  1010. .section-title .date { font-size: 24rpx; color: #666; }
  1011. .rfid-card {
  1012. background: #f6f6f6; border: 2rpx dashed #ccc;
  1013. border-radius: 20rpx; text-align: center;
  1014. }
  1015. .scan-icon { font-size: 50rpx; color: #007aff; margin-bottom: 10rpx; display: block; }
  1016. .btn-group { display: flex; gap: 20rpx; margin: 20rpx 0; flex-wrap: wrap; justify-content: center; }
  1017. .scan-btn { background: #007aff; color: white; flex: 1; min-width: 200rpx; padding: 20rpx; border-radius: 12rpx; font-size: 28rpx; }
  1018. .manual-btn { background: #ccc; color: black; flex: 1; min-width: 200rpx; padding: 20rpx; border-radius: 12rpx; font-size: 28rpx; }
  1019. .form-item { margin-bottom: 20rpx; }
  1020. .form-item .label { display: block; font-weight: bold; margin-bottom: 10rpx; }
  1021. .input-box { width: 100%; border: 1rpx solid #ccc; border-radius: 10rpx; background: #fff; }
  1022. .status-options { display: flex; gap: 20rpx; margin-top: 10rpx; }
  1023. .status-option {
  1024. flex: 1; border: 1rpx solid #ccc; border-radius: 10rpx;
  1025. padding: 20rpx; text-align: center; color: #666;
  1026. }
  1027. .status-option.active { border-color: #007aff; color: #007aff; }
  1028. .sv {
  1029. height: 300rpx;
  1030. margin-top: 20rpx;
  1031. }
  1032. </style>