face-photo.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <template>
  2. <view class="photo-container" :class="{'loaded': isPageLoaded}">
  3. <!-- 标题 -->
  4. <view class="photo-header">
  5. <text class="photo-title">拍摄照片</text>
  6. <text class="photo-subtitle">我们将用于身份核验,请正对摄像头</text>
  7. </view>
  8. <!-- 模式选择 -->
  9. <view class="mode-selector">
  10. <view class="mode-option" :class="{'active': mode === 'photo'}" @click="switchMode('photo')">拍照</view>
  11. <view class="mode-option" :class="{'active': mode === 'video'}" @click="switchMode('video')">录制视频</view>
  12. </view>
  13. <!-- 照片/视频预览区域 -->
  14. <view class="photo-preview">
  15. <!-- 使用camera组件替代静态图片 -->
  16. <camera v-if="!mediaSource" device-position="front" flash="auto" class="camera"
  17. :mode="mode" @error="handleCameraError"></camera>
  18. <image v-else-if="mode === 'photo'" class="preview-image" :src="mediaSource" mode="aspectFit"></image>
  19. <video v-else-if="mode === 'video'" class="preview-video" :src="mediaSource"
  20. controls autoplay></video>
  21. <view class="face-outline"></view>
  22. <!-- 视频录制时间显示 -->
  23. <view v-if="mode === 'video' && isRecording" class="recording-indicator">
  24. <view class="recording-dot"></view>
  25. <text class="recording-time">{{formatTime(recordingTime)}}</text>
  26. </view>
  27. </view>
  28. <!-- 操作按钮 -->
  29. <view v-if="!mediaSource" class="capture-btn-container">
  30. <button v-if="mode === 'photo'" class="capture-btn" @click="takePhoto">拍照</button>
  31. <button v-else-if="mode === 'video' && !isRecording" class="capture-btn" @click="startRecording">开始录制</button>
  32. <button v-else-if="mode === 'video' && isRecording" class="stop-btn" @click="stopRecording">停止录制</button>
  33. </view>
  34. <view v-else class="btn-group">
  35. <button class="retry-btn" @click="retakeMedia">重新{{mode === 'photo' ? '拍照' : '录制'}}</button>
  36. <button class="start-btn" @click="startInterview">开始面试</button>
  37. </view>
  38. </view>
  39. </template>
  40. <script>
  41. export default {
  42. data() {
  43. return {
  44. mediaSource: '', // 拍摄的照片或视频路径
  45. isCameraReady: false,
  46. cameraContext: null,
  47. isPageLoaded: false, // 添加页面加载状态标记
  48. mode: 'photo', // 默认为拍照模式,可选值:photo, video
  49. isRecording: false, // 是否正在录制视频
  50. recordingTime: 0, // 录制时间(秒)
  51. recordingTimer: null, // 录制计时器
  52. }
  53. },
  54. onReady() {
  55. // 相机组件初始化完成后创建相机上下文
  56. this.cameraContext = uni.createCameraContext();
  57. // 设置页面已加载完成
  58. setTimeout(() => {
  59. this.isPageLoaded = true;
  60. }, 100);
  61. },
  62. methods: {
  63. // 切换拍照/录制模式
  64. switchMode(newMode) {
  65. if (this.mediaSource) {
  66. // 如果已经有拍摄内容,需要先清除
  67. this.retakeMedia();
  68. }
  69. this.mode = newMode;
  70. },
  71. // 处理相机错误
  72. handleCameraError(e) {
  73. console.error('相机错误:', e);
  74. uni.showToast({
  75. title: '相机初始化失败,请检查权限设置',
  76. icon: 'none'
  77. });
  78. },
  79. // 拍照方法
  80. takePhoto() {
  81. if (!this.cameraContext) {
  82. uni.showToast({
  83. title: '相机未就绪',
  84. icon: 'none'
  85. });
  86. return;
  87. }
  88. uni.showLoading({
  89. title: '拍照中...'
  90. });
  91. this.cameraContext.takePhoto({
  92. quality: 'high',
  93. success: (res) => {
  94. this.mediaSource = res.tempImagePath;
  95. uni.hideLoading();
  96. },
  97. fail: (err) => {
  98. console.error('拍照失败:', err);
  99. uni.hideLoading();
  100. uni.showToast({
  101. title: '拍照失败,请重试',
  102. icon: 'none'
  103. });
  104. }
  105. });
  106. },
  107. // 开始录制视频
  108. startRecording() {
  109. if (!this.cameraContext) {
  110. uni.showToast({
  111. title: '相机未就绪',
  112. icon: 'none'
  113. });
  114. return;
  115. }
  116. this.isRecording = true;
  117. this.recordingTime = 0;
  118. // 开始计时
  119. this.recordingTimer = setInterval(() => {
  120. this.recordingTime++;
  121. // 限制最长录制时间为60秒
  122. if (this.recordingTime >= 60) {
  123. this.stopRecording();
  124. }
  125. }, 1000);
  126. this.cameraContext.startRecord({
  127. success: () => {
  128. console.log('开始录制视频');
  129. },
  130. fail: (err) => {
  131. console.error('开始录制失败:', err);
  132. this.isRecording = false;
  133. clearInterval(this.recordingTimer);
  134. uni.showToast({
  135. title: '录制失败,请重试',
  136. icon: 'none'
  137. });
  138. }
  139. });
  140. },
  141. // 停止录制视频
  142. stopRecording() {
  143. if (!this.isRecording) return;
  144. clearInterval(this.recordingTimer);
  145. this.isRecording = false;
  146. uni.showLoading({
  147. title: '处理中...'
  148. });
  149. this.cameraContext.stopRecord({
  150. success: (res) => {
  151. this.mediaSource = res.tempVideoPath;
  152. uni.hideLoading();
  153. },
  154. fail: (err) => {
  155. console.error('停止录制失败:', err);
  156. uni.hideLoading();
  157. uni.showToast({
  158. title: '视频保存失败,请重试',
  159. icon: 'none'
  160. });
  161. }
  162. });
  163. },
  164. // 格式化时间显示 (秒 -> MM:SS)
  165. formatTime(seconds) {
  166. const minutes = Math.floor(seconds / 60);
  167. const remainingSeconds = seconds % 60;
  168. return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
  169. },
  170. // 重新拍照或录制
  171. retakeMedia() {
  172. this.mediaSource = '';
  173. this.recordingTime = 0;
  174. },
  175. startInterview() {
  176. if (!this.mediaSource) {
  177. uni.showToast({
  178. title: `请先完成${this.mode === 'photo' ? '拍照' : '视频录制'}`,
  179. icon: 'none'
  180. });
  181. return;
  182. }
  183. uni.showLoading({
  184. title: '验证中...'
  185. });
  186. // 这里可以添加将照片或视频上传到服务器进行身份验证的代码
  187. // 例如:this.uploadMedia();
  188. setTimeout(() => {
  189. uni.hideLoading();
  190. uni.navigateTo({
  191. url: '/pages/camera/camera',
  192. fail: (err) => {
  193. console.error('页面跳转失败:', err);
  194. uni.showToast({
  195. title: '页面跳转失败',
  196. icon: 'none'
  197. });
  198. }
  199. });
  200. }, 1500);
  201. }
  202. // 上传媒体文件方法(示例)
  203. /*
  204. uploadMedia() {
  205. uni.uploadFile({
  206. url: 'https://your-api-endpoint.com/upload',
  207. filePath: this.mediaSource,
  208. name: this.mode === 'photo' ? 'photo' : 'video',
  209. success: (res) => {
  210. const data = JSON.parse(res.data);
  211. if (data.success) {
  212. // 验证成功,继续流程
  213. } else {
  214. // 验证失败,提示用户
  215. uni.showToast({
  216. title: data.message || '验证失败,请重试',
  217. icon: 'none'
  218. });
  219. }
  220. },
  221. fail: (err) => {
  222. console.error('上传失败:', err);
  223. uni.showToast({
  224. title: '网络错误,请重试',
  225. icon: 'none'
  226. });
  227. }
  228. });
  229. }
  230. */
  231. }
  232. }
  233. </script>
  234. <style>
  235. .photo-container {
  236. display: flex;
  237. flex-direction: column;
  238. min-height: 100vh;
  239. background-color: #f5f7fa;
  240. padding: 30rpx;
  241. opacity: 0; /* 初始设置为不可见 */
  242. transition: opacity 0.3s ease; /* 添加过渡效果 */
  243. }
  244. .photo-container.loaded {
  245. opacity: 1; /* 加载完成后显示 */
  246. }
  247. .photo-header {
  248. display: flex;
  249. flex-direction: column;
  250. align-items: center;
  251. margin-bottom: 20rpx;
  252. }
  253. .photo-title {
  254. font-size: 36rpx;
  255. font-weight: bold;
  256. color: #333;
  257. margin-bottom: 10rpx;
  258. }
  259. .photo-subtitle {
  260. font-size: 28rpx;
  261. color: #666;
  262. }
  263. /* 模式选择器样式 */
  264. .mode-selector {
  265. display: flex;
  266. justify-content: center;
  267. margin-bottom: 30rpx;
  268. background-color: #eaeaea;
  269. border-radius: 40rpx;
  270. padding: 6rpx;
  271. width: 80%;
  272. align-self: center;
  273. }
  274. .mode-option {
  275. flex: 1;
  276. text-align: center;
  277. padding: 16rpx 0;
  278. font-size: 28rpx;
  279. color: #666;
  280. border-radius: 36rpx;
  281. transition: all 0.3s;
  282. }
  283. .mode-option.active {
  284. background-color: #6c5ce7;
  285. color: #fff;
  286. }
  287. .photo-preview {
  288. position: relative;
  289. width: 100%;
  290. height: 800rpx;
  291. background-color: #000;
  292. border-radius: 20rpx;
  293. overflow: hidden;
  294. margin-bottom: 40rpx;
  295. display: flex;
  296. justify-content: center;
  297. align-items: center;
  298. }
  299. .preview-image, .preview-video {
  300. width: 100%;
  301. height: 100%;
  302. object-fit: cover;
  303. }
  304. .face-outline {
  305. position: absolute;
  306. top: 50%;
  307. left: 50%;
  308. transform: translate(-50%, -50%);
  309. width: 400rpx;
  310. height: 500rpx;
  311. border: 4rpx dashed #fff;
  312. border-radius: 200rpx;
  313. pointer-events: none;
  314. }
  315. .camera {
  316. width: 100%;
  317. height: 100%;
  318. }
  319. /* 录制指示器 */
  320. .recording-indicator {
  321. position: absolute;
  322. top: 30rpx;
  323. right: 30rpx;
  324. display: flex;
  325. align-items: center;
  326. background-color: rgba(0, 0, 0, 0.5);
  327. padding: 10rpx 20rpx;
  328. border-radius: 30rpx;
  329. }
  330. .recording-dot {
  331. width: 20rpx;
  332. height: 20rpx;
  333. background-color: #ff0000;
  334. border-radius: 50%;
  335. margin-right: 10rpx;
  336. animation: blink 1s infinite;
  337. }
  338. .recording-time {
  339. color: #fff;
  340. font-size: 28rpx;
  341. }
  342. @keyframes blink {
  343. 0% { opacity: 1; }
  344. 50% { opacity: 0.3; }
  345. 100% { opacity: 1; }
  346. }
  347. .capture-btn-container {
  348. width: 100%;
  349. display: flex;
  350. justify-content: center;
  351. margin-top: 60rpx;
  352. }
  353. .capture-btn {
  354. width: 100%;
  355. height: 90rpx;
  356. line-height: 90rpx;
  357. background-color: #6c5ce7;
  358. color: #fff;
  359. border-radius: 45rpx;
  360. font-size: 32rpx;
  361. }
  362. .stop-btn {
  363. width: 100%;
  364. height: 90rpx;
  365. line-height: 90rpx;
  366. background-color: #e74c3c;
  367. color: #fff;
  368. border-radius: 45rpx;
  369. font-size: 32rpx;
  370. }
  371. .btn-group {
  372. display: flex;
  373. justify-content: space-between;
  374. width: 100%;
  375. margin-top: 60rpx;
  376. }
  377. .retry-btn {
  378. width: 45%;
  379. height: 90rpx;
  380. line-height: 90rpx;
  381. background-color: #fff;
  382. color: #6c5ce7;
  383. border: 1px solid #6c5ce7;
  384. border-radius: 45rpx;
  385. font-size: 32rpx;
  386. }
  387. .start-btn {
  388. width: 45%;
  389. height: 90rpx;
  390. line-height: 90rpx;
  391. background-color: #6c5ce7;
  392. color: #fff;
  393. border-radius: 45rpx;
  394. font-size: 32rpx;
  395. }
  396. </style>