index.vue 46 KB

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