vision-input.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. <template>
  2. <view class="page">
  3. <view class="bg-gradient"></view>
  4. <van-nav-bar :title="inputType === 'vision' ? '视力录入' : '屈光度录入'" left-arrow @click-left="onBack" fixed :z-index="999" />
  5. <view class="header">
  6. <view class="student-info">
  7. <view class="info-row">
  8. <image class="gender-icon" :src="studentInfo.gender === '男' ? '/static/images/icon/man.png' : '/static/images/icon/woman.png'" mode="aspectFit"></image>
  9. <text class="student-name">{{ studentInfo.name }}</text>
  10. </view>
  11. <view class="detail-row">
  12. <text>学校:{{ schoolName }}</text>
  13. </view>
  14. <view class="detail-row">
  15. <text>班级:{{ studentInfo.class }}</text>
  16. </view>
  17. <view class="detail-row">
  18. <text>身份证:{{ studentInfo.idCard }}</text>
  19. </view>
  20. </view>
  21. </view>
  22. <view class="content">
  23. <view class="section-header">
  24. <text class="section-title">眼科检查</text>
  25. </view>
  26. <view class="form-item" v-if="inputType === 'vision'">
  27. <text class="label">左眼远视力</text>
  28. <input class="value-input" v-model="formData.leftVision" type="digit" placeholder="请输入" @blur="formatVision('leftVision')" />
  29. </view>
  30. <view class="form-item" v-if="inputType === 'vision'">
  31. <text class="label">右眼远视力</text>
  32. <input class="value-input" v-model="formData.rightVision" type="digit" placeholder="请输入" @blur="formatVision('rightVision')" />
  33. </view>
  34. <view class="form-item" v-if="inputType === 'refraction'">
  35. <text class="label">左眼屈光度</text>
  36. <input class="value-input" v-model="formData.leftRefraction" type="number" placeholder="请输入" />
  37. </view>
  38. <view class="form-item" v-if="inputType === 'refraction'">
  39. <text class="label">右眼屈光度</text>
  40. <input class="value-input" v-model="formData.rightRefraction" type="number" placeholder="请输入" />
  41. </view>
  42. </view>
  43. <view class="footer">
  44. <view class="footer-btns">
  45. <van-button plain type="primary" round size="large" @click="onBack" icon="replay">返回</van-button>
  46. <van-button type="warning" round size="large" @click="handleScan" icon="scan">扫一扫</van-button>
  47. <van-button type="primary" round size="large" icon="success" @click="handleSave">保存</van-button>
  48. </view>
  49. </view>
  50. <van-popup v-model:show="showScanner" position="center" :style="{ width: '90%', height: '80%' }">
  51. <view class="scanner-popup">
  52. <view class="scanner-header">
  53. <text>扫描二维码</text>
  54. <van-icon name="cross" @click="closeScanner" />
  55. </view>
  56. <view class="scanner-body">
  57. <video ref="videoRef" class="scanner-video" autoplay playsinline></video>
  58. <canvas ref="canvasRef" style="display: none;"></canvas>
  59. </view>
  60. </view>
  61. </van-popup>
  62. </view>
  63. <scan-code ref="scan"></scan-code>
  64. </template>
  65. <script setup>
  66. import { inputVisionData, inputRefractionData } from '@/services/common'
  67. // import jsQR from 'jsqr'
  68. import scanCode from '@/components/easy-scancode/easy-scancode'
  69. const STORAGE_KEY = 'eye_examine_user_info'
  70. const userInfo = ref({})
  71. const studentInfo = ref({})
  72. const inputType = ref('')
  73. const visionDataId = ref('')
  74. const schoolName = ref('')
  75. const showScanner = ref(false)
  76. const videoRef = ref(null)
  77. const canvasRef = ref(null)
  78. let stream = null
  79. let animationId = null
  80. const formData = ref({
  81. leftVision: '',
  82. rightVision: '',
  83. leftRefraction: '',
  84. rightRefraction: ''
  85. })
  86. onLoad((options) => {
  87. studentInfo.value = {
  88. id: options.studentId || '',
  89. name: decodeURIComponent(options.studentName || ''),
  90. gender: decodeURIComponent(options.studentGender || ''),
  91. idCard: decodeURIComponent(options.studentIdCard || ''),
  92. classId: options.classId || '',
  93. class: `${decodeURIComponent(options.grade || '')} ${decodeURIComponent(options.className || '')}`
  94. }
  95. inputType.value = options.type || ''
  96. visionDataId.value = options.visionDataId || ''
  97. schoolName.value = decodeURIComponent(options.schoolName || '')
  98. userInfo.value = {
  99. name: decodeURIComponent(options.userName || ''),
  100. phone: decodeURIComponent(options.userPhone || ''),
  101. organization: decodeURIComponent(options.userOrg || '')
  102. }
  103. })
  104. const onBack = () => {
  105. uni.navigateBack({
  106. delta: 1
  107. })
  108. }
  109. const scan = ref(null)
  110. const handleScan = async () => {
  111. // showScanner.value = true
  112. // await nextTick()
  113. // startCamera()
  114. scan.value.start({
  115. success: (val, res) => {
  116. //val是扫描到的二维码内容
  117. console.log('扫描成功', val, res)
  118. //关闭当前页面并跳转到选择录入类型页面
  119. uni.redirectTo({
  120. url: `/pages/index/select-input-type?studentId=${val}`
  121. })
  122. },
  123. fail: (rej) => {
  124. console.log('扫描失败', rej)
  125. }
  126. })
  127. }
  128. const startCamera = async () => {
  129. try {
  130. stream = await navigator.mediaDevices.getUserMedia({
  131. video: { facingMode: 'environment' }
  132. })
  133. const video = videoRef.value
  134. if (video) {
  135. video.srcObject = stream
  136. video.play()
  137. scanQRCode()
  138. }
  139. } catch (error) {
  140. console.error('摄像头启动失败', error)
  141. uni.showToast({
  142. title: '摄像头启动失败',
  143. icon: 'none'
  144. })
  145. }
  146. }
  147. const scanQRCode = () => {
  148. const video = videoRef.value
  149. const canvas = canvasRef.value
  150. if (!video || !canvas) return
  151. const ctx = canvas.getContext('2d')
  152. const scan = () => {
  153. if (video.readyState === video.HAVE_ENOUGH_DATA) {
  154. canvas.width = video.videoWidth
  155. canvas.height = video.videoHeight
  156. ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
  157. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  158. const code = jsQR(imageData.data, imageData.width, imageData.height)
  159. if (code) {
  160. onDecode(code.data)
  161. return
  162. }
  163. }
  164. animationId = requestAnimationFrame(scan)
  165. }
  166. scan()
  167. }
  168. const onDecode = (result) => {
  169. closeScanner()
  170. console.log('扫码结果:', result)
  171. }
  172. const closeScanner = () => {
  173. if (animationId) {
  174. cancelAnimationFrame(animationId)
  175. animationId = null
  176. }
  177. if (stream) {
  178. stream.getTracks().forEach(track => track.stop())
  179. stream = null
  180. }
  181. showScanner.value = false
  182. }
  183. const formatVision = (field) => {
  184. const value = parseFloat(formData.value[field])
  185. if (!isNaN(value)) {
  186. formData.value[field] = value.toFixed(1)
  187. }
  188. }
  189. const handleSave = async () => {
  190. if (inputType.value === 'vision') {
  191. if (!formData.value.leftVision || !formData.value.rightVision) {
  192. return uni.showToast({
  193. title: '请填写完整视力信息',
  194. icon: 'none'
  195. })
  196. }
  197. try {
  198. const params = {
  199. student_id: parseInt(studentInfo.value.id),
  200. left_eye_vision: parseFloat(formData.value.leftVision),
  201. right_eye_vision: parseFloat(formData.value.rightVision),
  202. input_user: userInfo.value.name,
  203. operator_phone: userInfo.value.phone
  204. }
  205. if (visionDataId.value) {
  206. params.vision_data_id = visionDataId.value
  207. }
  208. const res = await inputVisionData(params)
  209. if (res && res.code === 200) {
  210. uni.showToast({
  211. title: '保存成功',
  212. icon: 'success',
  213. duration: 1000
  214. })
  215. // setTimeout(() => {
  216. // uni.navigateBack({ delta: 2 })
  217. // }, 1000)
  218. }
  219. } catch (error) {
  220. console.error('保存视力数据失败', error)
  221. }
  222. } else if (inputType.value === 'refraction') {
  223. if (!formData.value.leftRefraction || !formData.value.rightRefraction) {
  224. return uni.showToast({
  225. title: '请填写完整屈光度信息',
  226. icon: 'none'
  227. })
  228. }
  229. try {
  230. const params = {
  231. student_id: parseInt(studentInfo.value.id),
  232. left_eye_refraction: parseFloat(formData.value.leftRefraction),
  233. right_eye_refraction: parseFloat(formData.value.rightRefraction),
  234. input_user: userInfo.value.name,
  235. operator_phone: userInfo.value.phone
  236. }
  237. if (visionDataId.value) {
  238. params.vision_data_id = visionDataId.value
  239. }
  240. const res = await inputRefractionData(params)
  241. if (res && res.code === 200) {
  242. uni.showToast({
  243. title: '保存成功',
  244. icon: 'success',
  245. duration: 1000
  246. })
  247. setTimeout(() => {
  248. uni.navigateBack({ delta: 2 })
  249. }, 1000)
  250. }
  251. } catch (error) {
  252. console.error('保存屈光度数据失败', error)
  253. }
  254. }
  255. }
  256. </script>
  257. <style lang="scss" scoped>
  258. .page {
  259. min-height: 100vh;
  260. position: relative;
  261. }
  262. .bg-gradient {
  263. position: fixed;
  264. top: 0;
  265. left: 0;
  266. right: 0;
  267. bottom: 0;
  268. background: linear-gradient(180deg, #4A9FF5 0%, #E8F5FF 100%);
  269. z-index: 0;
  270. pointer-events: none;
  271. }
  272. .header {
  273. padding: 20rpx 30rpx;
  274. padding-top: calc(20rpx + 46px);
  275. position: relative;
  276. z-index: 1;
  277. }
  278. .student-info {
  279. background: rgba(255, 255, 255, 0.9);
  280. border-radius: 20rpx;
  281. padding: 30rpx;
  282. }
  283. .info-row {
  284. display: flex;
  285. align-items: center;
  286. margin-bottom: 20rpx;
  287. }
  288. .gender-icon {
  289. width: 40rpx;
  290. height: 40rpx;
  291. margin-right: 10rpx;
  292. }
  293. .student-name {
  294. font-size: 32rpx;
  295. font-weight: bold;
  296. color: #333;
  297. }
  298. .detail-row {
  299. font-size: 26rpx;
  300. color: #666;
  301. margin-top: 10rpx;
  302. }
  303. .content {
  304. padding: 30rpx;
  305. position: relative;
  306. z-index: 1;
  307. padding-bottom: 150rpx;
  308. }
  309. .section-header {
  310. display: flex;
  311. align-items: center;
  312. justify-content: space-between;
  313. margin-bottom: 20rpx;
  314. }
  315. .section-title {
  316. font-size: 32rpx;
  317. font-weight: bold;
  318. color: #333;
  319. }
  320. .form-item {
  321. background: #fff;
  322. border-radius: 20rpx;
  323. padding: 30rpx;
  324. margin-bottom: 20rpx;
  325. display: flex;
  326. align-items: center;
  327. justify-content: space-between;
  328. }
  329. .label {
  330. font-size: 28rpx;
  331. color: #333;
  332. }
  333. .value {
  334. font-size: 32rpx;
  335. font-weight: bold;
  336. color: #0063F5;
  337. flex: 1;
  338. text-align: center;
  339. }
  340. .value-input {
  341. font-size: 32rpx;
  342. font-weight: bold;
  343. color: #0063F5;
  344. flex: 1;
  345. text-align: center;
  346. }
  347. .edit-icon {
  348. width: 40rpx;
  349. height: 40rpx;
  350. }
  351. .footer {
  352. position: fixed;
  353. bottom: 0;
  354. left: 0;
  355. right: 0;
  356. padding: 30rpx;
  357. background: #fff;
  358. z-index: 10;
  359. }
  360. .footer-btns {
  361. display: flex;
  362. gap: 20rpx;
  363. button {
  364. flex: 1;
  365. }
  366. }
  367. .scanner-popup {
  368. width: 100%;
  369. height: 100%;
  370. display: flex;
  371. flex-direction: column;
  372. background: #000;
  373. }
  374. .scanner-header {
  375. display: flex;
  376. justify-content: space-between;
  377. align-items: center;
  378. padding: 20rpx;
  379. background: #fff;
  380. }
  381. .scanner-body {
  382. flex: 1;
  383. display: flex;
  384. align-items: center;
  385. justify-content: center;
  386. overflow: hidden;
  387. }
  388. .scanner-video {
  389. width: 100%;
  390. height: 100%;
  391. object-fit: cover;
  392. }
  393. </style>