vision-input.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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. const savedData = uni.getStorageSync(STORAGE_KEY)
  99. let savedInfo = {}
  100. if (savedData) {
  101. savedInfo = JSON.parse(savedData)
  102. }
  103. userInfo.value = {
  104. name: savedInfo.name || '',
  105. phone: savedInfo.phone || decodeURIComponent(options.userPhone || ''),
  106. organization: savedInfo.organization || ''
  107. }
  108. })
  109. const onBack = () => {
  110. uni.navigateBack({
  111. delta: 1
  112. })
  113. }
  114. const scan = ref(null)
  115. const handleScan = async () => {
  116. // showScanner.value = true
  117. // await nextTick()
  118. // startCamera()
  119. scan.value.start({
  120. success: (val, res) => {
  121. //val是扫描到的二维码内容
  122. console.log('扫描成功', val, res)
  123. //关闭当前页面并跳转到选择录入类型页面
  124. uni.redirectTo({
  125. url: `/pages/index/select-input-type?studentId=${val}`
  126. })
  127. },
  128. fail: (rej) => {
  129. console.log('扫描失败', rej)
  130. }
  131. })
  132. }
  133. const startCamera = async () => {
  134. try {
  135. stream = await navigator.mediaDevices.getUserMedia({
  136. video: { facingMode: 'environment' }
  137. })
  138. const video = videoRef.value
  139. if (video) {
  140. video.srcObject = stream
  141. video.play()
  142. scanQRCode()
  143. }
  144. } catch (error) {
  145. console.error('摄像头启动失败', error)
  146. uni.showToast({
  147. title: '摄像头启动失败',
  148. icon: 'none'
  149. })
  150. }
  151. }
  152. const scanQRCode = () => {
  153. const video = videoRef.value
  154. const canvas = canvasRef.value
  155. if (!video || !canvas) return
  156. const ctx = canvas.getContext('2d')
  157. const scan = () => {
  158. if (video.readyState === video.HAVE_ENOUGH_DATA) {
  159. canvas.width = video.videoWidth
  160. canvas.height = video.videoHeight
  161. ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
  162. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
  163. const code = jsQR(imageData.data, imageData.width, imageData.height)
  164. if (code) {
  165. onDecode(code.data)
  166. return
  167. }
  168. }
  169. animationId = requestAnimationFrame(scan)
  170. }
  171. scan()
  172. }
  173. const onDecode = (result) => {
  174. closeScanner()
  175. console.log('扫码结果:', result)
  176. }
  177. const closeScanner = () => {
  178. if (animationId) {
  179. cancelAnimationFrame(animationId)
  180. animationId = null
  181. }
  182. if (stream) {
  183. stream.getTracks().forEach(track => track.stop())
  184. stream = null
  185. }
  186. showScanner.value = false
  187. }
  188. const formatVision = (field) => {
  189. const value = parseFloat(formData.value[field])
  190. if (!isNaN(value)) {
  191. formData.value[field] = value.toFixed(1)
  192. }
  193. }
  194. const handleSave = async () => {
  195. if (inputType.value === 'vision') {
  196. if (!formData.value.leftVision || !formData.value.rightVision) {
  197. return uni.showToast({
  198. title: '请填写完整视力信息',
  199. icon: 'none'
  200. })
  201. }
  202. try {
  203. const params = {
  204. student_id: parseInt(studentInfo.value.id),
  205. left_eye_vision: parseFloat(formData.value.leftVision),
  206. right_eye_vision: parseFloat(formData.value.rightVision),
  207. input_user: userInfo.value.name,
  208. operator_phone: userInfo.value.phone
  209. }
  210. if (visionDataId.value) {
  211. params.vision_data_id = visionDataId.value
  212. }
  213. const res = await inputVisionData(params)
  214. if (res && res.code === 200) {
  215. uni.showToast({
  216. title: '保存成功',
  217. icon: 'success',
  218. duration: 1000
  219. })
  220. setTimeout(() => {
  221. uni.navigateBack({ delta: 1 })
  222. }, 1000)
  223. }
  224. } catch (error) {
  225. console.error('保存视力数据失败', error)
  226. }
  227. } else if (inputType.value === 'refraction') {
  228. if (!formData.value.leftRefraction || !formData.value.rightRefraction) {
  229. return uni.showToast({
  230. title: '请填写完整屈光度信息',
  231. icon: 'none'
  232. })
  233. }
  234. try {
  235. const params = {
  236. student_id: parseInt(studentInfo.value.id),
  237. left_eye_refraction: parseFloat(formData.value.leftRefraction),
  238. right_eye_refraction: parseFloat(formData.value.rightRefraction),
  239. input_user: userInfo.value.name,
  240. operator_phone: userInfo.value.phone
  241. }
  242. if (visionDataId.value) {
  243. params.vision_data_id = visionDataId.value
  244. }
  245. const res = await inputRefractionData(params)
  246. if (res && res.code === 200) {
  247. uni.showToast({
  248. title: '保存成功',
  249. icon: 'success',
  250. duration: 1000
  251. })
  252. setTimeout(() => {
  253. uni.navigateBack({ delta: 1 })
  254. }, 1000)
  255. }
  256. } catch (error) {
  257. console.error('保存屈光度数据失败', error)
  258. }
  259. }
  260. }
  261. </script>
  262. <style lang="scss" scoped>
  263. .page {
  264. min-height: 100vh;
  265. position: relative;
  266. }
  267. .bg-gradient {
  268. position: fixed;
  269. top: 0;
  270. left: 0;
  271. right: 0;
  272. bottom: 0;
  273. background: linear-gradient(180deg, #4A9FF5 0%, #E8F5FF 100%);
  274. z-index: 0;
  275. pointer-events: none;
  276. }
  277. .header {
  278. padding: 20rpx 30rpx;
  279. padding-top: calc(20rpx + 46px);
  280. position: relative;
  281. z-index: 1;
  282. }
  283. .student-info {
  284. background: rgba(255, 255, 255, 0.9);
  285. border-radius: 20rpx;
  286. padding: 30rpx;
  287. }
  288. .info-row {
  289. display: flex;
  290. align-items: center;
  291. margin-bottom: 20rpx;
  292. }
  293. .gender-icon {
  294. width: 40rpx;
  295. height: 40rpx;
  296. margin-right: 10rpx;
  297. }
  298. .student-name {
  299. font-size: 32rpx;
  300. font-weight: bold;
  301. color: #333;
  302. }
  303. .detail-row {
  304. font-size: 26rpx;
  305. color: #666;
  306. margin-top: 10rpx;
  307. }
  308. .content {
  309. padding: 30rpx;
  310. position: relative;
  311. z-index: 1;
  312. padding-bottom: 150rpx;
  313. }
  314. .section-header {
  315. display: flex;
  316. align-items: center;
  317. justify-content: space-between;
  318. margin-bottom: 20rpx;
  319. }
  320. .section-title {
  321. font-size: 32rpx;
  322. font-weight: bold;
  323. color: #333;
  324. }
  325. .form-item {
  326. background: #fff;
  327. border-radius: 20rpx;
  328. padding: 30rpx;
  329. margin-bottom: 20rpx;
  330. display: flex;
  331. align-items: center;
  332. justify-content: space-between;
  333. }
  334. .label {
  335. font-size: 28rpx;
  336. color: #333;
  337. }
  338. .value {
  339. font-size: 32rpx;
  340. font-weight: bold;
  341. color: #0063F5;
  342. flex: 1;
  343. text-align: center;
  344. }
  345. .value-input {
  346. font-size: 32rpx;
  347. font-weight: bold;
  348. color: #0063F5;
  349. flex: 1;
  350. text-align: center;
  351. }
  352. .edit-icon {
  353. width: 40rpx;
  354. height: 40rpx;
  355. }
  356. .footer {
  357. position: fixed;
  358. bottom: 0;
  359. left: 0;
  360. right: 0;
  361. padding: 30rpx;
  362. background: #fff;
  363. z-index: 10;
  364. }
  365. .footer-btns {
  366. display: flex;
  367. gap: 20rpx;
  368. button {
  369. flex: 1;
  370. }
  371. }
  372. .scanner-popup {
  373. width: 100%;
  374. height: 100%;
  375. display: flex;
  376. flex-direction: column;
  377. background: #000;
  378. }
  379. .scanner-header {
  380. display: flex;
  381. justify-content: space-between;
  382. align-items: center;
  383. padding: 20rpx;
  384. background: #fff;
  385. }
  386. .scanner-body {
  387. flex: 1;
  388. display: flex;
  389. align-items: center;
  390. justify-content: center;
  391. overflow: hidden;
  392. }
  393. .scanner-video {
  394. width: 100%;
  395. height: 100%;
  396. object-fit: cover;
  397. }
  398. </style>