|
@@ -0,0 +1,401 @@
|
|
|
+<template>
|
|
|
+ <view class="camera-container">
|
|
|
+ <!-- 全屏相机预览 -->
|
|
|
+ <camera class="camera-preview"
|
|
|
+ :resolution="'medium'"
|
|
|
+ :device-position="devicePosition"
|
|
|
+ :flash="flash"
|
|
|
+ :frame-size="frameSize"
|
|
|
+ @stop="handleStop"
|
|
|
+ @error="handleError"
|
|
|
+ @initdone="handleInitDone">
|
|
|
+ </camera>
|
|
|
+
|
|
|
+ <!-- 左上角:闪光灯图标按钮 -->
|
|
|
+ <view class="top-left">
|
|
|
+ <button class="flash-btn" @click="switchFlash">
|
|
|
+ <text class="icon">{{ flash === 'torch' ? '💡' : '🔦' }}</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 右上角:图像质量设置 -->
|
|
|
+ <view class="top-right">
|
|
|
+ <view class="quality-setting">
|
|
|
+ <text class="setting-label">成像质量</text>
|
|
|
+ <radio-group class="quality-group" @change="takePhotoQualityChange">
|
|
|
+ <radio class="quality-radio" value="normal" :checked="quality === 'normal'">普通</radio>
|
|
|
+ <radio class="quality-radio" value="high" :checked="quality === 'high'">高清</radio>
|
|
|
+ <radio class="quality-radio" value="original" :checked="quality === 'original'">原图</radio>
|
|
|
+ </radio-group>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 底部中间:圆形拍照按钮 -->
|
|
|
+ <view class="bottom-center">
|
|
|
+ <button class="shoot-btn" @click="handleTakePhoto">
|
|
|
+ <view class="shoot-inner"></view>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 左下角:相册预览 -->
|
|
|
+ <view class="bottom-left">
|
|
|
+ <view class="album-preview" @click="handleScanCode">
|
|
|
+ <image class="preview-img" v-if="imageSrc" :src="imageSrc"></image>
|
|
|
+ <text class="preview-tip" v-else>相册</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 下方:缩放控制(位置保持不变) -->
|
|
|
+ <view class="zoom-control">
|
|
|
+ <text class="setting-label">预览缩放</text>
|
|
|
+ <view class="zoom-container">
|
|
|
+ <slider class="zoom-slider"
|
|
|
+ :disabled="maxZoom <= 1"
|
|
|
+ :show-value="true"
|
|
|
+ :min="1"
|
|
|
+ :max="maxZoom"
|
|
|
+ :value="1"
|
|
|
+ @change="zoomSliderChange" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ devicePosition: "back",
|
|
|
+ flash: "off",
|
|
|
+ frameSize: "medium",
|
|
|
+ listener: null as CameraContextCameraFrameListener | null,
|
|
|
+ maxZoom: 0,
|
|
|
+ imageSrc: "",
|
|
|
+ quality: "normal",
|
|
|
+ timeout: 30,
|
|
|
+ compressed: false,
|
|
|
+ videoSrc: "",
|
|
|
+ startRecordStatus: false,
|
|
|
+ remain: 0,
|
|
|
+ intervalId: -1,
|
|
|
+ timeoutStr: '30'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad() {
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ handleScanCode() {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: "/pages/component/camera/camera-scan-code"
|
|
|
+ })
|
|
|
+ },
|
|
|
+ switchDevicePosition() {
|
|
|
+ this.devicePosition = this.devicePosition == "back" ? "front" : "back"
|
|
|
+ },
|
|
|
+
|
|
|
+ switchFlash() {
|
|
|
+ this.flash = this.flash == "torch" ? "off" : "torch"
|
|
|
+ },
|
|
|
+
|
|
|
+ handleStop(e : UniCameraStopEvent) {
|
|
|
+ console.log("stop", e.detail);
|
|
|
+ },
|
|
|
+ handleError(e : UniCameraErrorEvent) {
|
|
|
+ console.log("error", e.detail);
|
|
|
+ },
|
|
|
+ handleInitDone(e : UniCameraInitDoneEvent) {
|
|
|
+ console.log("initdone", e.detail);
|
|
|
+ this.maxZoom = e.detail.maxZoom ?? 0
|
|
|
+ },
|
|
|
+ zoomSliderChange(event : UniSliderChangeEvent) {
|
|
|
+ const value = event.detail.value
|
|
|
+ const context = uni.createCameraContext();
|
|
|
+ context?.setZoom({
|
|
|
+ zoom: value,
|
|
|
+ success: (e : any) => {
|
|
|
+ console.log(e);
|
|
|
+ }
|
|
|
+ } as CameraContextSetZoomOptions)
|
|
|
+ },
|
|
|
+ handleTakePhoto() {
|
|
|
+ const context = uni.createCameraContext();
|
|
|
+ context?.takePhoto({
|
|
|
+ quality: this.quality,
|
|
|
+ selfieMirror: false,
|
|
|
+ success: (res : CameraContextTakePhotoResult) => {
|
|
|
+ console.log("res.tempImagePath", res.tempImagePath);
|
|
|
+ this.imageSrc = res.tempImagePath ?? ''
|
|
|
+ },
|
|
|
+ fail: (e : any) => {
|
|
|
+ console.log("take photo", e);
|
|
|
+ }
|
|
|
+ } as CameraContextTakePhotoOptions)
|
|
|
+ },
|
|
|
+ takePhotoQualityChange(event : UniRadioGroupChangeEvent) {
|
|
|
+ this.quality = event.detail.value
|
|
|
+ console.log("quality", this.quality);
|
|
|
+ },
|
|
|
+
|
|
|
+ setOnFrameListener() {
|
|
|
+ const context = uni.createCameraContext();
|
|
|
+ this.listener = context?.onCameraFrame((frame : CameraContextOnCameraFrame) => {
|
|
|
+ console.log("OnFrame :", frame);
|
|
|
+ })
|
|
|
+ },
|
|
|
+ startFrameListener() {
|
|
|
+ this.listener?.start({
|
|
|
+ success: (res : any) => {
|
|
|
+ console.log("startFrameListener success", res);
|
|
|
+ }
|
|
|
+ } as CameraContextCameraFrameListenerStartOptions)
|
|
|
+
|
|
|
+ },
|
|
|
+ stopFrameListener() {
|
|
|
+ this.listener?.stop({
|
|
|
+ success: (res : any) => {
|
|
|
+ console.log("stopFrameListener success", res);
|
|
|
+ }
|
|
|
+ } as CameraContextCameraFrameListenerStopOptions)
|
|
|
+ },
|
|
|
+ startRecord() {
|
|
|
+ const context = uni.createCameraContext();
|
|
|
+ let timeout = this.getTimeout()
|
|
|
+ this.timeout = timeout
|
|
|
+ context?.startRecord({
|
|
|
+ timeout: timeout,
|
|
|
+ selfieMirror: false,
|
|
|
+ timeoutCallback: (res : any) => {
|
|
|
+ console.log("timeoutCallback", res);
|
|
|
+ this.startRecordStatus = false
|
|
|
+ if (typeof res != "string") {
|
|
|
+ const result = res as CameraContextStartRecordTimeoutResult
|
|
|
+ this.videoSrc = result.tempVideoPath ?? ''
|
|
|
+ }
|
|
|
+ clearInterval(this.intervalId)
|
|
|
+ },
|
|
|
+ success: (res : any) => {
|
|
|
+ this.startRecordStatus = true
|
|
|
+ console.log("start record success", res);
|
|
|
+ this.remain = timeout
|
|
|
+ this.intervalId = setInterval(() => {
|
|
|
+ if (this.remain <= 0) {
|
|
|
+ clearInterval(this.intervalId)
|
|
|
+ } else {
|
|
|
+ this.remain--
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+ },
|
|
|
+ fail: (res : any) => {
|
|
|
+ console.log("start record fail", res);
|
|
|
+ this.startRecordStatus = false
|
|
|
+ this.remain = 0
|
|
|
+ clearInterval(this.intervalId)
|
|
|
+ }
|
|
|
+ } as CameraContextStartRecordOptions)
|
|
|
+ },
|
|
|
+ stopRecord() {
|
|
|
+ this.startRecordStatus = false
|
|
|
+ const context = uni.createCameraContext();
|
|
|
+ context?.stopRecord({
|
|
|
+ compressed: this.compressed,
|
|
|
+ success: (res : CameraContextStopRecordResult) => {
|
|
|
+ console.log("stop record success", res);
|
|
|
+ this.videoSrc = res.tempVideoPath ?? ''
|
|
|
+ },
|
|
|
+ fail: (res : any) => {
|
|
|
+ console.log("stop record fail", res);
|
|
|
+ }
|
|
|
+ } as CameraContextStopRecordOptions)
|
|
|
+ clearInterval(this.intervalId)
|
|
|
+ this.remain = 0
|
|
|
+ },
|
|
|
+ startRecordCompressChange(event : UniRadioGroupChangeEvent) {
|
|
|
+ this.compressed = parseInt(event.detail.value) == 1
|
|
|
+ },
|
|
|
+ timeoutInput(event : UniInputEvent) {
|
|
|
+ this.timeoutStr = event.detail.value
|
|
|
+ },
|
|
|
+ getTimeout() : number {
|
|
|
+ let value = parseInt(this.timeoutStr)
|
|
|
+ // #ifdef APP-ANDROID
|
|
|
+ if (value == NaN) {
|
|
|
+ // #endif
|
|
|
+ // #ifndef APP-ANDROID
|
|
|
+ if (value !== value) {
|
|
|
+ // #endif
|
|
|
+ return 30
|
|
|
+ } else {
|
|
|
+ if (value < 1) {
|
|
|
+ return 1
|
|
|
+ } else if (value > 60 * 5) {
|
|
|
+ return 60 * 5
|
|
|
+ } else {
|
|
|
+ return value
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scope>
|
|
|
+ /* 基础容器 */
|
|
|
+ .camera-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ background-color: #000;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 相机预览 */
|
|
|
+ .camera-preview {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 左上角:闪光灯按钮 */
|
|
|
+ .top-left {
|
|
|
+ position: absolute;
|
|
|
+ top: 40rpx;
|
|
|
+ left: 40rpx;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ .flash-btn {
|
|
|
+ width: 100rpx;
|
|
|
+ height: 100rpx;
|
|
|
+ border-radius: 100rpx;
|
|
|
+ background-color: rgba(0, 0, 0, 0.4);
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .icon {
|
|
|
+ font-size: 48rpx;
|
|
|
+ color: #e0e0e0; /* 银色 */
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 右上角:图像质量设置 */
|
|
|
+ .top-right {
|
|
|
+ position: absolute;
|
|
|
+ top: 40rpx;
|
|
|
+ right: 40rpx;
|
|
|
+ z-index: 10;
|
|
|
+ background-color: rgba(0, 0, 0, 0.4);
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 底部中间:拍照按钮 */
|
|
|
+ .bottom-center {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 240rpx;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ .shoot-btn {
|
|
|
+ width: 180rpx;
|
|
|
+ height: 180rpx;
|
|
|
+ border-radius: 180rpx;
|
|
|
+ background-color: #ffffff;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0;
|
|
|
+ border: 8rpx solid rgba(255, 255, 255, 0.3);
|
|
|
+ box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ .shoot-inner {
|
|
|
+ width: 140rpx;
|
|
|
+ height: 140rpx;
|
|
|
+ border-radius: 140rpx;
|
|
|
+ background-color: #f0f0f0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 左下角:相册预览 */
|
|
|
+ .bottom-left {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 240rpx;
|
|
|
+ left: 40rpx;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ .album-preview {
|
|
|
+ width: 130rpx;
|
|
|
+ height: 130rpx;
|
|
|
+ border: 4rpx solid #e0e0e0;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ background-color: rgba(255, 255, 255, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-tip {
|
|
|
+ color: #e0e0e0; /* 银色 */
|
|
|
+ font-size: 26rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 下方:缩放控制 */
|
|
|
+ .zoom-control {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 40rpx;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ padding: 0 40rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ .setting-label {
|
|
|
+ display: block;
|
|
|
+ color: #e0e0e0; /* 银色 */
|
|
|
+ font-size: 28rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 质量选择样式 */
|
|
|
+ .quality-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ .quality-radio {
|
|
|
+ color: #e0e0e0; /* 银色 */
|
|
|
+ font-size: 26rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 缩放控制样式 */
|
|
|
+ .zoom-container {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10rpx 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .zoom-slider {
|
|
|
+ width: 100%;
|
|
|
+ height: 4px;
|
|
|
+ }
|
|
|
+</style>
|