vision-input.vue 9.5 KB

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