interview.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158
  1. <template>
  2. <view class="container">
  3. <!-- <view class="header">
  4. <text class="title">手部照片采集</text>
  5. </view> -->
  6. <view class="photo-container">
  7. <view class="instruction">
  8. <text>{{ instructions[currentStep] }}</text>
  9. </view>
  10. <view class="preview-container">
  11. <image v-if="photos[currentStep]" :src="photos[currentStep]" mode="aspectFit" class="preview-image"></image>
  12. <view v-else class="empty-preview">
  13. <text>{{ previewTexts[currentStep] }}</text>
  14. </view>
  15. </view>
  16. <view class="thumbnails">
  17. <view v-for="(photo, index) in photos" :key="index"
  18. class="thumbnail-item"
  19. :class="{ active: currentStep === index, completed: photo }">
  20. <image v-if="photo" :src="photo" mode="aspectFill" class="thumbnail-image" @click="previewPhoto(index)"></image>
  21. <view v-else class="thumbnail-placeholder">{{ index + 1 }}</view>
  22. </view>
  23. </view>
  24. </view>
  25. <view class="controls" v-if="allPhotosCompleted">
  26. <button class="camera-btn" @click="takePhoto">{{ photos[currentStep] ? '重新拍摄' : '拍摄照片' }}</button>
  27. <view class="navigation-btns" v-if="allPhotosCompleted">
  28. <!-- <button class="nav-btn prev" :disabled="currentStep === 0" @click="prevStep">上一步</button> -->
  29. <button class="nav-btn next" :disabled="!canGoNext" @click="nextStep">
  30. {{ allPhotosCompleted ? '完成' : '下一步' }}
  31. </button>
  32. </view>
  33. </view>
  34. <!-- 为小程序环境添加camera组件 -->
  35. <view v-if="!isH5 && showCamera" class="camera-container">
  36. <cover-view class="gesture-hint">
  37. <cover-view class="gesture-hint-text">{{ gestureHints[currentStep] }}</cover-view>
  38. </cover-view>
  39. <camera
  40. :device-position="cameraPosition"
  41. flash="auto"
  42. @error="handleCameraError"
  43. class="camera-component"
  44. :mode="cameraMode"
  45. frame-size="medium"
  46. ></camera>
  47. <!-- 添加手势引导蒙层 -->
  48. <view class="gesture-overlay">
  49. <!-- 添加右上角引导图标 -->
  50. <view class="guide-icon" :style="currentStep>=3?'left:30rpx':'right:30rpx'">
  51. <!-- <image :src="guideIcon[currentStep]" class="guide-image"></image> -->
  52. <video
  53. :src="guideIcon[currentStep]"
  54. id="mpVideo"
  55. autoplay
  56. class="mp-video-player"
  57. :controls="false"
  58. loop
  59. muted
  60. object-fit="contain"
  61. ></video>
  62. <!-- <text class="guide-text">引导</text> -->
  63. </view>
  64. <image
  65. :src="gestureOverlays[currentStep]"
  66. class="gesture-image"
  67. ></image>
  68. </view>
  69. <cover-view class="camera-controls">
  70. <cover-view class="capture-btn" @tap="capturePhotoMP"></cover-view>
  71. <!-- <cover-view class="camera-buttons" v-if="allPhotosCompleted">
  72. <cover-view class="cancel-btn" @tap="cancelCamera">取消</cover-view>
  73. </cover-view> -->
  74. </cover-view>
  75. </view>
  76. <!-- 为浏览器环境添加视频元素 -->
  77. <view v-if="isH5" class="camera-container" v-show="showCamera">
  78. <!-- <video ref="videoElement" class="camera-video" autoplay></video> -->
  79. <canvas ref="canvasElement" class="camera-canvas" style="display: none;"></canvas>
  80. <!-- 添加H5环境的手势引导蒙层 -->
  81. <view class="gesture-overlay">
  82. <!-- 添加右上角引导图标 -->
  83. <view class="guide-icon" :style="currentStep>=3?'left:30rpx':'right:30rpx'">
  84. <image :src="guideIcon[currentStep]" class="guide-image"></image>
  85. <!-- <text class="guide-text">引导</text> -->
  86. </view>
  87. <image
  88. :src="gestureOverlays[currentStep]"
  89. class="gesture-image"
  90. ></image>
  91. <view class="gesture-hint">
  92. <text class="gesture-hint-text">{{ gestureHints[currentStep] }}</text>
  93. </view>
  94. </view>
  95. <view class="camera-buttons">
  96. <button class="capture-btn" @click="capturePhoto">拍照</button>
  97. <button class="switch-camera-btn" @click="switchCamera">切换摄像头</button>
  98. <button class="cancel-btn" @click="cancelCamera">取消</button>
  99. </view>
  100. </view>
  101. </view>
  102. </template>
  103. <script>
  104. import { apiBaseUrl } from '@/common/config.js'; // Import the base URL from a config file
  105. export default {
  106. data() {
  107. return {
  108. currentStep: 0,
  109. photos: [null, null, null, null, null, null], // 存储6张照片:左手正面、左手反面、左手握拳、右手正面、右手反面、右手握拳
  110. instructions: [
  111. '请将左手掌正面朝上,保持手指自然伸展,确保整个手掌清晰可见',
  112. '请将左手掌反面朝上,保持手指自然伸展,确保整个手背清晰可见',
  113. '请握紧左手拳头,使拳面朝向相机,确保整个拳头清晰可见',
  114. '请将右手掌正面朝上,保持手指自然伸展,确保整个手掌清晰可见',
  115. '请将右手掌反面朝上,保持手指自然伸展,确保整个手背清晰可见',
  116. '请握紧右手拳头,使拳面朝向相机,确保整个拳头清晰可见'
  117. ],
  118. previewTexts: [
  119. '左手掌正面照片预览区',
  120. '左手掌反面照片预览区',
  121. '左手握拳照片预览区',
  122. '右手掌正面照片预览区',
  123. '右手掌反面照片预览区',
  124. '右手握拳照片预览区'
  125. ],
  126. photoTypes: ['left_palm', 'left_back', 'left_fist', 'right_palm', 'right_back', 'right_fist'], // 照片类型标识
  127. isH5: false,
  128. showCamera: false,
  129. mediaStream: null,
  130. cameraContext: null,
  131. cameraMode: 'normal', // 添加相机模式
  132. cameraInitRetries: 0, // 添加重试计数器
  133. gestureOverlays: [
  134. 'https://data.qicai321.com/minlong/18590378-6792-4a26-be18-ad9f2bcc4159.png',
  135. 'https://data.qicai321.com/minlong/e08cff2b-3b1d-478f-a62b-766b38445a16.png',
  136. 'https://data.qicai321.com/minlong/d30ccab7-9cfe-4386-bf0e-ac1f0045bd76.png',
  137. 'https://data.qicai321.com/minlong/8068a698-ac40-4a5d-a737-54dba47a668d.png',
  138. 'https://data.qicai321.com/minlong/f04a1f7e-4f79-49c7-9a02-bbad296672ea.png',
  139. 'https://data.qicai321.com/minlong/1d9bbc36-2fb8-4489-bff6-c36e7a31bd37.png'
  140. /* '/static/images/palm_overlay.png', // 手掌正面引导图
  141. '/static/images/back_overlay.png', // 手掌反面引导图
  142. '/static/images/fist_overlay.png' // 握拳引导图 */
  143. ],
  144. gestureHints: [
  145. '请将左手掌对准轮廓线',
  146. '请将左手背对准轮廓线',
  147. '请将左手拳头对准轮廓线',
  148. '请将右手掌对准轮廓线',
  149. '请将右手背对准轮廓线',
  150. '请将右手拳头对准轮廓线'
  151. ],
  152. photoLinks: [null, null, null, null, null, null], // 存储上传后的图片链接
  153. cameraPosition: 'front', // 修改默认为前置摄像头
  154. guideIcon: [
  155. 'http://data.qicai321.com/minlong/fd080e3b-67be-4ce7-87a3-58d047cfbbb1.mp4',
  156. 'http://data.qicai321.com/minlong/394a858c-672a-44bd-8206-450913863900.mp4',
  157. 'http://data.qicai321.com/minlong/37153e68-a8f0-4a93-b677-5e5678379243.mp4',
  158. 'http://data.qicai321.com/minlong/ebf6e43f-ffdf-49bb-8b00-adeff9ce1b56.mp4',
  159. 'http://data.qicai321.com/minlong/e0ca45e2-af3e-42a0-9dd1-4de545e9c720.mp4',
  160. 'http://data.qicai321.com/minlong/77028dd6-22bb-41b4-8879-19699e8b1dbb.mp4',
  161. ]
  162. }
  163. },
  164. computed: {
  165. allPhotosCompleted() {
  166. // 检查是否所有照片都已拍摄完成
  167. return this.photos.every(photo => photo !== null);
  168. },
  169. canGoNext() {
  170. // 如果是最后一步,检查所有照片是否都已完成
  171. if (this.currentStep === 5) {
  172. return this.photos.every(photo => photo !== null);
  173. }
  174. // 其他步骤只检查当前步骤的照片
  175. return this.photos[this.currentStep] !== null;
  176. }
  177. },
  178. onLoad() {
  179. // 判断当前平台
  180. // #ifdef H5
  181. this.isH5 = true;
  182. // #endif
  183. // 检查相机权限
  184. // #ifdef MP-WEIXIN
  185. uni.authorize({
  186. scope: 'scope.camera',
  187. success: () => {
  188. console.log('相机权限已授权');
  189. this.takePhoto();
  190. },
  191. fail: () => {
  192. uni.showModal({
  193. title: '提示',
  194. content: '请授权相机权限以完成手部照片采集',
  195. success: (res) => {
  196. if (res.confirm) {
  197. uni.openSetting();
  198. }
  199. }
  200. });
  201. }
  202. });
  203. // #endif
  204. },
  205. beforeDestroy() {
  206. // 在组件销毁前停止摄像头流
  207. this.stopMediaStream();
  208. },
  209. methods: {
  210. takePhoto() {
  211. // 记录当前是否为重新拍摄
  212. const isRetake = this.photos[this.currentStep] !== null;
  213. // 先重置当前步骤的状态
  214. this.$set(this.photos, this.currentStep, null);
  215. this.$set(this.photoLinks, this.currentStep, null);
  216. // 如果是重新拍摄,显示提示
  217. if (isRetake) {
  218. uni.showToast({
  219. title: '重新拍摄第' + (this.currentStep + 1) + '张照片',
  220. icon: 'none',
  221. duration: 1500
  222. });
  223. }
  224. // 根据平台选择不同的拍照方法
  225. if (this.isH5) {
  226. this.startCamera();
  227. } else {
  228. // 小程序环境使用camera组件
  229. this.showCamera = true;
  230. this.cameraInitRetries = 0; // 重置重试计数器
  231. this.$nextTick(() => {
  232. // 创建相机上下文
  233. // #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO
  234. try {
  235. this.cameraContext = uni.createCameraContext();
  236. console.log('相机上下文创建成功');
  237. } catch (err) {
  238. console.error('创建相机上下文失败:', err);
  239. this.handleCameraInitError();
  240. }
  241. // #endif
  242. });
  243. }
  244. },
  245. uploadPhoto(filePathOrData, type) {
  246. console.log(`准备上传${type}类型的手部照片`);
  247. uni.showLoading({
  248. title: '上传中...'
  249. });
  250. // 获取当前步骤索引,确保上传到正确的位置
  251. const currentIndex = this.currentStep;
  252. // 获取用户openid
  253. const userInfo = uni.getStorageSync('userInfo');
  254. const openid = userInfo ? JSON.parse(userInfo).openid || '' : '';
  255. const tenant_id = uni.getStorageSync('tenant_id') || '1';
  256. // 检查用户信息是否完整
  257. if (!tenant_id) {
  258. uni.hideLoading();
  259. uni.showToast({
  260. title: '用户信息不完整,请重新登录',
  261. icon: 'none'
  262. });
  263. return;
  264. }
  265. // 定义更详细的照片描述
  266. const photoDescriptions = {
  267. 'left_palm': '展示完整的左手手掌',
  268. 'left_back': '展示完整的左手手背',
  269. 'left_fist': '展示完整的左手握拳',
  270. 'right_palm': '展示完整的右手手掌',
  271. 'right_back': '展示完整的右手手背',
  272. 'right_fist': '展示完整的右手握拳'
  273. };
  274. // 获取当前照片类型的详细描述
  275. const description = photoDescriptions[type] || `手部照片-${type}`;
  276. // 如果是H5环境且是base64数据
  277. if (this.isH5 && filePathOrData.startsWith('data:image')) {
  278. // 将base64转为blob对象
  279. const base64Data = filePathOrData.split(',')[1];
  280. const blob = this.base64ToBlob(base64Data, 'image/jpeg');
  281. const formData = new FormData();
  282. // 确保文件参数名称正确
  283. formData.append('photo', blob, `${type}.jpg`);
  284. formData.append('application_id', uni.getStorageSync('appId'));
  285. formData.append('tenant_id', tenant_id);
  286. formData.append('description', description);
  287. console.log('H5上传参数:', {
  288. application_id: uni.getStorageSync('appId'),
  289. tenant_id: tenant_id,
  290. description: description
  291. });
  292. // 使用fetch直接上传到 job/upload_posture_photo 接口
  293. fetch(`${apiBaseUrl}/job/upload_posture_photo`, {
  294. method: 'POST',
  295. body: formData
  296. })
  297. .then(response => response.json())
  298. .then(result => {
  299. console.log('照片上传成功:', result);
  300. // 保存图片链接到对应类型的索引位置
  301. const index = this.photoTypes.indexOf(type);
  302. if (index !== -1 && result.data && result.data.url) {
  303. this.$set(this.photoLinks, index, result.data.url);
  304. console.log(`已保存${type}类型照片链接:`, result.data.url);
  305. }
  306. uni.hideLoading();
  307. uni.showToast({
  308. title: '照片上传成功',
  309. icon: 'success',
  310. duration: 1500,
  311. success: () => {
  312. // 判断是否是重新拍摄的照片
  313. const isRetake = this.photos.filter(photo => photo !== null).length > currentIndex + 1;
  314. // 如果是重新拍摄,不自动进入下一步
  315. if (!isRetake) {
  316. // 判断是否是最后一步
  317. if (currentIndex === 5) {
  318. // 最后一步,显示提示但不自动进入下一步
  319. uni.showToast({
  320. title: '最后一张照片已完成,请点击完成按钮提交',
  321. icon: 'success',
  322. duration: 2000
  323. });
  324. } else {
  325. // 不是最后一步,自动进入下一步
  326. setTimeout(() => {
  327. this.autoNextStep();
  328. }, 1500);
  329. }
  330. }
  331. }
  332. });
  333. })
  334. .catch(error => {
  335. console.error('照片上传失败:', error);
  336. uni.hideLoading();
  337. uni.showToast({
  338. title: '照片上传失败,请重试',
  339. icon: 'none'
  340. });
  341. });
  342. } else {
  343. // 小程序环境的上传逻辑
  344. console.log(`小程序环境上传文件路径:`, filePathOrData);
  345. console.log('小程序上传参数:', {
  346. application_id: uni.getStorageSync('appId'),
  347. tenant_id: tenant_id,
  348. description: description
  349. });
  350. uni.uploadFile({
  351. url: `${apiBaseUrl}/job/upload_posture_photo`,
  352. filePath: filePathOrData,
  353. name: 'photo', // 确保文件参数名称正确
  354. formData: { // 使用formData替代data以确保参数正确传递
  355. 'application_id': uni.getStorageSync('appId'),
  356. 'tenant_id': tenant_id,
  357. 'description': description
  358. },
  359. success: (uploadRes) => {
  360. console.log('照片上传成功:', uploadRes);
  361. // 解析返回的JSON字符串
  362. let result;
  363. try {
  364. if (typeof uploadRes.data === 'string') {
  365. result = JSON.parse(uploadRes.data);
  366. } else {
  367. result = uploadRes.data;
  368. }
  369. console.log('解析后的上传结果:', result);
  370. // 保存图片链接到对应类型的索引位置
  371. const index = this.photoTypes.indexOf(type);
  372. if (index !== -1 && result.data && result.data.url) {
  373. this.$set(this.photoLinks, index, result.data.url);
  374. console.log(`已保存${type}类型照片链接:`, result.data.url);
  375. }
  376. } catch (e) {
  377. console.error('解析上传结果失败:', e);
  378. }
  379. uni.hideLoading();
  380. uni.showToast({
  381. title: '照片上传成功',
  382. icon: 'success',
  383. duration: 1500,
  384. success: () => {
  385. // 判断是否是重新拍摄的照片
  386. const isRetake = this.photos.filter(photo => photo !== null).length > currentIndex + 1;
  387. // 如果是重新拍摄,不自动进入下一步
  388. if (!isRetake) {
  389. // 判断是否是最后一步
  390. if (currentIndex === 5) {
  391. // 最后一步,显示提示但不自动进入下一步
  392. uni.showToast({
  393. title: '最后一张照片已完成,请点击完成按钮提交',
  394. icon: 'success',
  395. duration: 2000
  396. });
  397. } else {
  398. // 不是最后一步,自动进入下一步
  399. setTimeout(() => {
  400. this.autoNextStep();
  401. }, 1500);
  402. }
  403. }
  404. }
  405. });
  406. },
  407. fail: (err) => {
  408. console.error('照片上传失败:', err);
  409. uni.hideLoading();
  410. uni.showToast({
  411. title: '照片上传失败,请重试',
  412. icon: 'none'
  413. });
  414. },
  415. complete: () => {
  416. uni.hideLoading();
  417. }
  418. });
  419. }
  420. },
  421. prevStep() {
  422. if (this.currentStep > 0) {
  423. this.currentStep--;
  424. }
  425. },
  426. nextStep() {
  427. // 如果所有照片都已拍摄完成,点击"完成"按钮时提交
  428. if (this.allPhotosCompleted) {
  429. // 所有照片都已拍摄完成,进行提交
  430. this.submitAllPhotos();
  431. return;
  432. }
  433. // 否则进入下一步
  434. if (this.currentStep < 5) {
  435. this.currentStep++;
  436. }
  437. },
  438. previewPhoto(index) {
  439. // 如果照片存在,有两种操作:
  440. // 1. 在预览区显示该照片
  441. // 2. 切换到该步骤,允许重新拍摄
  442. if (this.photos[index]) {
  443. // 切换到对应的步骤
  444. this.currentStep = index;
  445. // 显示操作选项
  446. uni.showActionSheet({
  447. itemList: ['在预览区查看', '重新拍摄此照片'],
  448. success: (res) => {
  449. if (res.tapIndex === 0) {
  450. // 选择了预览,不需要额外操作,因为已经切换到对应步骤
  451. // 如果需要全屏预览,可以使用以下代码
  452. uni.previewImage({
  453. urls: [this.photos[index]],
  454. current: this.photos[index]
  455. });
  456. } else if (res.tapIndex === 1) {
  457. // 选择了重新拍摄
  458. this.takePhoto(); // 调用拍照方法重新拍摄当前步骤的照片
  459. }
  460. }
  461. });
  462. } else {
  463. // 如果照片不存在,直接切换到该步骤
  464. this.currentStep = index;
  465. // 可以选择是否自动开始拍照
  466. // this.takePhoto();
  467. }
  468. },
  469. submitAllPhotos() {
  470. // 检查是否所有照片都已拍摄
  471. if (this.allPhotosCompleted) {
  472. uni.showLoading({
  473. title: '处理中...'
  474. });
  475. // 获取必要参数
  476. const application_id = uni.getStorageSync('appId');
  477. const tenant_id = uni.getStorageSync('tenant_id') || '1';
  478. // 调用视觉分析接口
  479. uni.request({
  480. url: `${apiBaseUrl}/api/system/job/trigger_visual_analysis?application_id=${application_id}&tenant_id=${tenant_id}`,
  481. method: 'POST',
  482. success: (res) => {
  483. console.log('视觉分析触发成功:', res);
  484. // 继续处理跳转逻辑
  485. uni.hideLoading();
  486. uni.showToast({
  487. title: '手部照片采集完成',
  488. icon: 'success',
  489. duration: 2000,
  490. success: () => {
  491. // 延迟跳转,让用户看到成功提示
  492. setTimeout(() => {
  493. // 使用redirectTo替代navigateTo,避免页面栈溢出
  494. uni.redirectTo({
  495. url: '/pages/interview_success/interview_success'
  496. });
  497. }, 100);
  498. }
  499. });
  500. },
  501. fail: (err) => {
  502. console.error('视觉分析触发失败:', err);
  503. uni.hideLoading();
  504. uni.showToast({
  505. title: '处理失败,请重试',
  506. icon: 'none'
  507. });
  508. }
  509. });
  510. } else {
  511. uni.showToast({
  512. title: '请完成所有照片拍摄',
  513. icon: 'none'
  514. });
  515. }
  516. },
  517. // H5环境下启动摄像头
  518. startCamera() {
  519. this.showCamera = true;
  520. // 确保浏览器支持getUserMedia
  521. if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
  522. // 根据当前摄像头位置设置facingMode
  523. const facingMode = this.cameraPosition === 'front' ? 'user' : 'environment';
  524. navigator.mediaDevices.getUserMedia({
  525. video: { facingMode: facingMode }
  526. })
  527. .then(stream => {
  528. this.mediaStream = stream;
  529. // 等待DOM更新后再设置视频源
  530. this.$nextTick(() => {
  531. const videoElement = this.$refs.videoElement;
  532. if (videoElement) {
  533. videoElement.srcObject = stream;
  534. }
  535. });
  536. })
  537. .catch(error => {
  538. console.error('获取摄像头失败:', error);
  539. uni.showToast({
  540. title: '无法访问摄像头,请检查权限设置',
  541. icon: 'none'
  542. });
  543. this.showCamera = false;
  544. });
  545. } else {
  546. uni.showToast({
  547. title: '您的浏览器不支持摄像头功能',
  548. icon: 'none'
  549. });
  550. this.showCamera = false;
  551. }
  552. },
  553. // H5环境下拍照
  554. capturePhoto() {
  555. console.log('执行H5拍照方法');
  556. const videoElement = this.$refs.videoElement;
  557. const canvasElement = this.$refs.canvasElement;
  558. if (videoElement && canvasElement) {
  559. try {
  560. const context = canvasElement.getContext('2d');
  561. // 设置canvas尺寸与视频一致
  562. canvasElement.width = videoElement.videoWidth;
  563. canvasElement.height = videoElement.videoHeight;
  564. // 在canvas上绘制当前视频帧
  565. context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
  566. // 将canvas内容转为base64图片
  567. const photoData = canvasElement.toDataURL('image/jpeg');
  568. // 更新照片数据
  569. this.$set(this.photos, this.currentStep, photoData);
  570. // 上传照片
  571. this.uploadPhoto(photoData, this.photoTypes[this.currentStep]);
  572. // 关闭摄像头
  573. this.stopMediaStream();
  574. this.showCamera = false;
  575. } catch (err) {
  576. console.error('H5拍照失败:', err);
  577. uni.showToast({
  578. title: '拍照失败,请重试',
  579. icon: 'none'
  580. });
  581. }
  582. }
  583. },
  584. // 小程序环境下拍照
  585. capturePhotoMP() {
  586. console.log('执行小程序拍照方法');
  587. if (!this.cameraContext) {
  588. console.error('相机上下文不存在');
  589. this.handleCameraInitError();
  590. return;
  591. }
  592. try {
  593. uni.showLoading({
  594. title: '拍照中...',
  595. mask: true
  596. });
  597. this.cameraContext.takePhoto({
  598. quality: 'high',
  599. success: (res) => {
  600. console.log('拍照成功:', res);
  601. uni.hideLoading();
  602. // 先清空当前照片,确保状态一致
  603. this.$set(this.photos, this.currentStep, null);
  604. this.$set(this.photoLinks, this.currentStep, null);
  605. // 然后设置新照片
  606. this.$set(this.photos, this.currentStep, res.tempImagePath);
  607. // 上传照片
  608. this.uploadPhoto(res.tempImagePath, this.photoTypes[this.currentStep]);
  609. // 关闭相机
  610. this.showCamera = false;
  611. },
  612. fail: (err) => {
  613. uni.hideLoading();
  614. console.error('拍照失败:', err);
  615. uni.showToast({
  616. title: '拍照失败,请重试',
  617. icon: 'none'
  618. });
  619. // 尝试使用替代方法
  620. this.fallbackToChooseImage();
  621. }
  622. });
  623. } catch (err) {
  624. uni.hideLoading();
  625. console.error('takePhoto方法执行失败:', err);
  626. this.fallbackToChooseImage();
  627. }
  628. },
  629. // 添加相机初始化错误处理
  630. handleCameraInitError() {
  631. if (this.cameraInitRetries < 3) {
  632. this.cameraInitRetries++;
  633. uni.showToast({
  634. title: `相机初始化失败,正在重试(${this.cameraInitRetries}/3)`,
  635. icon: 'none'
  636. });
  637. setTimeout(() => {
  638. this.showCamera = false;
  639. setTimeout(() => {
  640. this.takePhoto();
  641. }, 500);
  642. }, 1000);
  643. } else {
  644. uni.showToast({
  645. title: '相机初始化失败,将使用系统相机',
  646. icon: 'none'
  647. });
  648. this.fallbackToChooseImage();
  649. }
  650. },
  651. // 添加备用的系统相机方法
  652. fallbackToChooseImage() {
  653. this.showCamera = false;
  654. // 使用系统相机作为备选方案
  655. uni.chooseImage({
  656. count: 1,
  657. sourceType: ['camera'],
  658. success: (res) => {
  659. // 更新当前步骤的照片
  660. this.$set(this.photos, this.currentStep, res.tempFilePaths[0]);
  661. // 上传照片
  662. this.uploadPhoto(res.tempFilePaths[0], this.photoTypes[this.currentStep]);
  663. },
  664. fail: (err) => {
  665. console.error('选择图片失败:', err);
  666. uni.showToast({
  667. title: '拍照失败,请重试',
  668. icon: 'none'
  669. });
  670. }
  671. });
  672. },
  673. // 修改相机错误处理方法
  674. handleCameraError(e) {
  675. console.error('相机错误:', e.detail);
  676. // 记录更详细的错误信息
  677. const errorInfo = JSON.stringify(e.detail);
  678. console.log('详细错误信息:', errorInfo);
  679. uni.showToast({
  680. title: '相机启动失败,将使用系统相机',
  681. icon: 'none'
  682. });
  683. // 使用备用方法
  684. this.fallbackToChooseImage();
  685. },
  686. // 取消拍照 - 更新为同时支持H5和小程序
  687. cancelCamera() {
  688. if (this.isH5) {
  689. this.stopMediaStream();
  690. }
  691. this.showCamera = false;
  692. // 重置当前步骤的照片状态,确保需要重新拍照
  693. this.$set(this.photos, this.currentStep, null);
  694. // 重置当前步骤的照片链接
  695. this.$set(this.photoLinks, this.currentStep, null);
  696. },
  697. // 停止摄像头流
  698. stopMediaStream() {
  699. if (this.mediaStream) {
  700. this.mediaStream.getTracks().forEach(track => {
  701. track.stop();
  702. });
  703. this.mediaStream = null;
  704. }
  705. },
  706. // base64转blob工具方法
  707. base64ToBlob(base64, mimeType) {
  708. const byteCharacters = atob(base64);
  709. const byteArrays = [];
  710. for (let offset = 0; offset < byteCharacters.length; offset += 512) {
  711. const slice = byteCharacters.slice(offset, offset + 512);
  712. const byteNumbers = new Array(slice.length);
  713. for (let i = 0; i < slice.length; i++) {
  714. byteNumbers[i] = slice.charCodeAt(i);
  715. }
  716. const byteArray = new Uint8Array(byteNumbers);
  717. byteArrays.push(byteArray);
  718. }
  719. return new Blob(byteArrays, { type: mimeType });
  720. },
  721. retryCamera() {
  722. this.showCamera = false;
  723. setTimeout(() => {
  724. this.takePhoto();
  725. }, 500);
  726. },
  727. // 可以添加一个方法来切换蒙层显示
  728. toggleGestureOverlay() {
  729. // 实现蒙层显示/隐藏的逻辑
  730. // 如果需要的话
  731. },
  732. // 切换摄像头
  733. switchCamera() {
  734. // 切换摄像头位置
  735. this.cameraPosition = this.cameraPosition === 'front' ? 'back' : 'front';
  736. // 如果是H5环境,需要重新启动摄像头
  737. if (this.isH5 && this.showCamera) {
  738. this.stopMediaStream();
  739. this.startCamera();
  740. }
  741. },
  742. // 添加自动进入下一步的方法
  743. autoNextStep() {
  744. // 检查是否所有照片都已拍摄完成
  745. const allPhotosCompleted = this.photos.every(photo => photo !== null);
  746. if (allPhotosCompleted) {
  747. // 所有照片都已拍摄完成,显示提示,但不自动提交
  748. // 让用户点击"完成"按钮来提交
  749. uni.showToast({
  750. title: '所有照片已完成,请点击完成按钮提交',
  751. icon: 'success',
  752. duration: 2000
  753. });
  754. return;
  755. }
  756. // 如果当前不是最后一步,进入下一步
  757. if (this.currentStep < 5) {
  758. this.currentStep++;
  759. // 自动开始拍摄下一张照片
  760. this.takePhoto();
  761. }
  762. // 移除自动提交逻辑,让用户点击"完成"按钮来提交
  763. }
  764. }
  765. }
  766. </script>
  767. <style>
  768. .container {
  769. display: flex;
  770. flex-direction: column;
  771. height: 94vh;
  772. background-color: #f5f5f5;
  773. }
  774. .header {
  775. padding: 20rpx;
  776. background-color: #007AFF;
  777. text-align: center;
  778. }
  779. .title {
  780. color: #ffffff;
  781. font-size: 36rpx;
  782. font-weight: bold;
  783. }
  784. .photo-container {
  785. flex: 1;
  786. padding: 20rpx;
  787. display: flex;
  788. flex-direction: column;
  789. }
  790. .instruction {
  791. padding: 20rpx;
  792. background-color: #E5E5EA;
  793. border-radius: 10rpx;
  794. margin-bottom: 20rpx;
  795. }
  796. .preview-container {
  797. flex: 1;
  798. display: flex;
  799. justify-content: center;
  800. align-items: center;
  801. background-color: #E5E5EA;
  802. border-radius: 10rpx;
  803. margin-bottom: 20rpx;
  804. overflow: hidden;
  805. }
  806. .preview-image {
  807. width: 100%;
  808. height: 100%;
  809. }
  810. .empty-preview {
  811. display: flex;
  812. justify-content: center;
  813. align-items: center;
  814. width: 100%;
  815. height: 100%;
  816. color: #999;
  817. }
  818. .thumbnails {
  819. display: flex;
  820. justify-content: space-around;
  821. margin-bottom: 20rpx;
  822. }
  823. .thumbnail-item {
  824. width: 150rpx;
  825. height: 150rpx;
  826. border-radius: 10rpx;
  827. overflow: hidden;
  828. background-color: #E5E5EA;
  829. display: flex;
  830. justify-content: center;
  831. align-items: center;
  832. border: 4rpx solid transparent;
  833. }
  834. .thumbnail-item.active {
  835. border-color: #007AFF;
  836. }
  837. .thumbnail-item.completed {
  838. background-color: #ffffff;
  839. }
  840. .thumbnail-image {
  841. width: 100%;
  842. height: 100%;
  843. }
  844. .thumbnail-placeholder {
  845. font-size: 40rpx;
  846. color: #999;
  847. }
  848. .controls {
  849. padding: 20rpx;
  850. background-color: #ffffff;
  851. border-top: 1px solid #0039b3;
  852. }
  853. .camera-btn {
  854. width: 100%;
  855. height: 80rpx;
  856. line-height: 80rpx;
  857. text-align: center;
  858. background-color: #0039b3;
  859. color: white;
  860. border-radius: 40rpx;
  861. margin-bottom: 20rpx;
  862. }
  863. .navigation-btns {
  864. display: flex;
  865. justify-content: space-between;
  866. }
  867. .nav-btn {
  868. width: 100%;
  869. height: 80rpx;
  870. line-height: 80rpx;
  871. text-align: center;
  872. border-radius: 40rpx;
  873. }
  874. .nav-btn.prev {
  875. background-color: #E5E5EA;
  876. color: #333;
  877. }
  878. .nav-btn.next {
  879. background-color: #34C759;
  880. color: white;
  881. }
  882. .nav-btn[disabled] {
  883. opacity: 0.5;
  884. }
  885. /* 浏览器摄像头相关样式 */
  886. .camera-container {
  887. position: fixed;
  888. top: 0;
  889. left: 0;
  890. width: 100%;
  891. height: 100%;
  892. background-color: #000;
  893. z-index: 999;
  894. display: flex;
  895. flex-direction: column;
  896. justify-content: center;
  897. align-items: center;
  898. }
  899. .camera-video {
  900. width: 100%;
  901. height: 70%;
  902. object-fit: cover;
  903. }
  904. .camera-canvas {
  905. display: none;
  906. }
  907. .capture-btn {
  908. width: 200rpx;
  909. height: 200rpx;
  910. border-radius: 50%;
  911. background-color: #ffffff;
  912. border: 10rpx solid #0039b3;
  913. margin: 30rpx 0;
  914. }
  915. .cancel-btn {
  916. width: 200rpx;
  917. height: 80rpx;
  918. line-height: 80rpx;
  919. background-color: rgba(255, 255, 255, 0.3);
  920. color: #ffffff;
  921. text-align: center;
  922. border-radius: 40rpx;
  923. }
  924. /* 小程序相机组件样式 */
  925. .camera-component {
  926. width: 100%;
  927. height: 100%;
  928. display: block;
  929. }
  930. .camera-controls {
  931. width: 100%;
  932. display: flex;
  933. flex-direction: column;
  934. align-items: center;
  935. position: absolute;
  936. bottom: 50rpx;
  937. z-index: 9999;
  938. }
  939. /* 添加手势引导蒙层样式 */
  940. .gesture-overlay {
  941. position: absolute;
  942. top: 0;
  943. left: 0;
  944. width: 100%;
  945. height: 80%;
  946. display: flex;
  947. flex-direction: column;
  948. justify-content: center;
  949. align-items: center;
  950. pointer-events: none; /* 允许点击穿透 */
  951. z-index: 10;
  952. }
  953. .gesture-image {
  954. width: 100%;
  955. height: 100%;
  956. /* opacity: 0.6; */
  957. }
  958. .gesture-hint {
  959. position: fixed;
  960. top: 1px;
  961. background-color: rgba(0, 0, 0, 0.5);
  962. padding: 10rpx 20rpx;
  963. border-radius: 10rpx;
  964. margin-top: 20rpx;
  965. z-index: 999;
  966. }
  967. .gesture-hint-text {
  968. color: white;
  969. font-size: 28rpx;
  970. text-align: center;
  971. }
  972. /* 添加切换摄像头按钮样式 */
  973. .switch-camera-btn {
  974. width: 200rpx;
  975. height: 80rpx;
  976. line-height: 80rpx;
  977. background-color: rgba(255, 255, 255, 0.3);
  978. color: #ffffff;
  979. text-align: center;
  980. border-radius: 40rpx;
  981. margin: 20rpx 0;
  982. }
  983. .camera-buttons {
  984. display: flex;
  985. flex-direction: row;
  986. justify-content: center;
  987. width: 100%;
  988. margin-top: 20rpx;
  989. }
  990. .capture-btn {
  991. width: 200rpx;
  992. height: 200rpx;
  993. border-radius: 50%;
  994. background-color: #ffffff;
  995. border: 10rpx solid #0039b3;
  996. margin: 30rpx 0;
  997. }
  998. .switch-camera-btn, .cancel-btn {
  999. width: 200rpx;
  1000. height: 80rpx;
  1001. line-height: 80rpx;
  1002. background-color: rgba(255, 255, 255, 0.3);
  1003. color: #ffffff;
  1004. text-align: center;
  1005. border-radius: 40rpx;
  1006. margin: 0 20rpx;
  1007. }
  1008. /* 添加右上角引导图标样式 */
  1009. .guide-icon {
  1010. position: absolute;
  1011. top: 30rpx;
  1012. right: 30rpx;
  1013. width: 192rpx;
  1014. height: 240rpx;
  1015. display: flex;
  1016. flex-direction: column;
  1017. align-items: center;
  1018. justify-content: center;
  1019. background-color: rgba(0, 0, 0, 0.5);
  1020. border-radius: 10rpx;
  1021. z-index: 11;
  1022. }
  1023. .mp-video-player{
  1024. width: 100%;
  1025. height: 100%;
  1026. border-radius: 10rpx;
  1027. }
  1028. .guide-image {
  1029. width: 160rpx;
  1030. height: 200rpx;
  1031. }
  1032. .guide-text {
  1033. color: white;
  1034. font-size: 24rpx;
  1035. margin-top: 5rpx;
  1036. }
  1037. </style>