|
|
@@ -0,0 +1,983 @@
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <el-dialog
|
|
|
+ :visible.sync="dialogVisible"
|
|
|
+ top="2vh"
|
|
|
+ :modal="false"
|
|
|
+ title="高拍仪"
|
|
|
+ width="800px"
|
|
|
+ :before-close="handleClose"
|
|
|
+ custom-class="camera-dialog"
|
|
|
+ >
|
|
|
+ <div class="camera-dialog-content">
|
|
|
+ <!-- 视频预览区 -->
|
|
|
+ <div
|
|
|
+ class="video-preview"
|
|
|
+ v-loading="videoLoading"
|
|
|
+ element-loading-text="正在加载摄像头..."
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ ref="videoRef"
|
|
|
+ :src="videoUrl"
|
|
|
+ alt="主摄像头"
|
|
|
+ class="video-stream"
|
|
|
+ @load="handleVideoLoad"
|
|
|
+ @error="handleVideoError"
|
|
|
+ v-show="!videoLoading"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 操作按钮区 -->
|
|
|
+ <div class="button-group">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :icon="'el-icon-refresh-left'"
|
|
|
+ @click="handleRotate(90)"
|
|
|
+ :loading="rotating"
|
|
|
+ >
|
|
|
+ 左转
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ icon="el-icon-camera"
|
|
|
+ @click="handleCapture"
|
|
|
+ :loading="capturing"
|
|
|
+ >
|
|
|
+ 拍照
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :icon="'el-icon-refresh-right'"
|
|
|
+ @click="handleRotate(270)"
|
|
|
+ :loading="rotating"
|
|
|
+ >
|
|
|
+ 右转
|
|
|
+ </el-button>
|
|
|
+ <el-popover placement="top" width="160" v-model="visible">
|
|
|
+ <p><el-input v-model="port"></el-input></p>
|
|
|
+ <div style="text-align: right; margin: 0">
|
|
|
+ <el-button size="mini" type="text" @click="visible = false"
|
|
|
+ >取消</el-button
|
|
|
+ >
|
|
|
+ <el-button type="primary" size="mini" @click="changePort"
|
|
|
+ >确定</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ <el-button icon="el-icon-s-tools" slot="reference"
|
|
|
+ >端口设置</el-button
|
|
|
+ >
|
|
|
+ </el-popover>
|
|
|
+ <!-- <el-button
|
|
|
+ type="warning"
|
|
|
+ icon="el-icon-search"
|
|
|
+ @click="handleScanQRCode"
|
|
|
+ :loading="scanning"
|
|
|
+ >
|
|
|
+ 识别二维码
|
|
|
+ </el-button> -->
|
|
|
+ <!-- <el-button
|
|
|
+ type="info"
|
|
|
+ icon="el-icon-document"
|
|
|
+ @click="handleReadIDCard"
|
|
|
+ :loading="readingIDCard"
|
|
|
+ >
|
|
|
+ 读取身份证
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ icon="el-icon-menu"
|
|
|
+ @click="handleScanBarcode"
|
|
|
+ :loading="scanningBarcode"
|
|
|
+ >
|
|
|
+ 条码识别
|
|
|
+ </el-button> -->
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 拍照历史记录 -->
|
|
|
+ <div class="photo-history">
|
|
|
+ <el-divider>
|
|
|
+ {{
|
|
|
+ multiple
|
|
|
+ ? '请选择照片(单击选择,双击预览)'
|
|
|
+ : '请选择照片(单击选择一张,双击预览)'
|
|
|
+ }}
|
|
|
+ </el-divider>
|
|
|
+ <div v-if="photoHistory.length > 0" class="thumbnail-container">
|
|
|
+ <div
|
|
|
+ v-for="(photo, index) in displayedPhotos"
|
|
|
+ :key="index"
|
|
|
+ class="thumbnail-item"
|
|
|
+ :class="{ selected: isPhotoSelected(photo) }"
|
|
|
+ @click="handleSelectPhoto(photo)"
|
|
|
+ @dblclick="handlePreviewPhoto(photo, index)"
|
|
|
+ >
|
|
|
+ <el-image
|
|
|
+ :src="photo"
|
|
|
+ class="thumbnail-image"
|
|
|
+ fit="cover"
|
|
|
+ :hide-on-click-modal="true"
|
|
|
+ :preview-disabled="true"
|
|
|
+ />
|
|
|
+ <div v-if="isPhotoSelected(photo)" class="selected-badge">
|
|
|
+ <i class="el-icon-check"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="photoHistory.length > 5" class="more-button-container">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-more"
|
|
|
+ @click="showAllPhotos = true"
|
|
|
+ class="more-button"
|
|
|
+ >
|
|
|
+ 更多 ({{ photoHistory.length - 5 }})
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="photo-history-empty">
|
|
|
+ <el-empty description="暂无拍照记录">
|
|
|
+ <template v-slot:image>
|
|
|
+ <i
|
|
|
+ class="el-icon-camera"
|
|
|
+ style="font-size: 80px; color: #c0c4cc"
|
|
|
+ ></i>
|
|
|
+ </template>
|
|
|
+ <template v-slot:description>
|
|
|
+ <p class="empty-tip">点击上方"拍照"按钮开始拍照</p>
|
|
|
+ </template>
|
|
|
+ </el-empty>
|
|
|
+ </div>
|
|
|
+ <div v-if="selectedPhotos.length > 0" class="selected-info">
|
|
|
+ {{
|
|
|
+ multiple
|
|
|
+ ? `已选择 ${selectedPhotos.length} 张图片`
|
|
|
+ : '已选择 1 张图片'
|
|
|
+ }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 身份证识别结果 -->
|
|
|
+ <div v-if="idCardResult" class="idcard-result">
|
|
|
+ <el-divider>身份证信息</el-divider>
|
|
|
+ <el-card class="idcard-card">
|
|
|
+ <div class="idcard-content">
|
|
|
+ <div class="idcard-photo" v-if="idCardResult.photoBase64">
|
|
|
+ <el-image
|
|
|
+ :src="'data:image/jpeg;base64,' + idCardResult.photoBase64"
|
|
|
+ fit="contain"
|
|
|
+ class="idcard-image"
|
|
|
+ :preview-src-list="idCardPreviewList"
|
|
|
+ :preview-teleported="true"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="idcard-info">
|
|
|
+ <el-descriptions :column="1" border>
|
|
|
+ <el-descriptions-item label="姓名">{{
|
|
|
+ idCardResult.name
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="身份证号">{{
|
|
|
+ idCardResult.cardID
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="性别">{{
|
|
|
+ idCardResult.sex
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="出生日期">{{
|
|
|
+ idCardResult.birthday
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="地址">{{
|
|
|
+ idCardResult.address
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="签发机关">{{
|
|
|
+ idCardResult.issueOrgan
|
|
|
+ }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="有效期限">
|
|
|
+ {{ idCardResult.validStart }} 至 {{ idCardResult.validEnd }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <div class="idcard-actions">
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ @click="handleCopyIDCard"
|
|
|
+ >复制信息</el-button
|
|
|
+ >
|
|
|
+ <el-button size="small" @click="idCardResult = null"
|
|
|
+ >关闭</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 条码识别结果 -->
|
|
|
+ <div v-if="barcodeResult" class="barcode-result">
|
|
|
+ <el-divider>条码识别结果</el-divider>
|
|
|
+ <el-alert
|
|
|
+ :title="barcodeResult"
|
|
|
+ type="success"
|
|
|
+ :closable="true"
|
|
|
+ @close="barcodeResult = ''"
|
|
|
+ show-icon
|
|
|
+ >
|
|
|
+ <template slot="default">
|
|
|
+ <div class="barcode-content">
|
|
|
+ <p><strong>识别内容:</strong></p>
|
|
|
+ <p class="barcode-text">{{ barcodeResult }}</p>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ @click="handleCopyBarcode"
|
|
|
+ >复制</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button @click="handleClose">取消</el-button>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ @click="handleClearAll"
|
|
|
+ :disabled="photoHistory.length === 0"
|
|
|
+ >
|
|
|
+ 清空图片
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" @click="handleConfirm">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 全部图片展示弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ :visible.sync="showAllPhotos"
|
|
|
+ :modal="false"
|
|
|
+ :title="
|
|
|
+ multiple
|
|
|
+ ? '全部拍照记录(单击选择,双击预览)'
|
|
|
+ : '全部拍照记录(单击选择一张,双击预览)'
|
|
|
+ "
|
|
|
+ width="90%"
|
|
|
+ :before-close="
|
|
|
+ () => {
|
|
|
+ showAllPhotos = false
|
|
|
+ }
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <div class="all-photos-container">
|
|
|
+ <div
|
|
|
+ v-for="(photo, index) in photoHistory"
|
|
|
+ :key="index"
|
|
|
+ class="all-photo-item"
|
|
|
+ :class="{ selected: isPhotoSelected(photo) }"
|
|
|
+ @click="handleSelectPhoto(photo)"
|
|
|
+ @dblclick="handlePreviewPhoto(photo, index)"
|
|
|
+ >
|
|
|
+ <el-image
|
|
|
+ :src="photo"
|
|
|
+ class="all-photo-image"
|
|
|
+ fit="cover"
|
|
|
+ :hide-on-click-modal="true"
|
|
|
+ :preview-disabled="true"
|
|
|
+ />
|
|
|
+ <div class="photo-index">第 {{ index + 1 }} 张</div>
|
|
|
+ <div v-if="isPhotoSelected(photo)" class="selected-badge-large">
|
|
|
+ <i class="el-icon-check"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button @click="showAllPhotos = false">取消</el-button>
|
|
|
+ <el-button type="danger" @click="handleClearAll">清空全部</el-button>
|
|
|
+ <el-button type="primary" @click="handleConfirm">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 全屏图片预览 -->
|
|
|
+ <div
|
|
|
+ v-if="showPreview"
|
|
|
+ class="fullscreen-preview"
|
|
|
+ @click.self="showPreview = false"
|
|
|
+ >
|
|
|
+ <div class="preview-content">
|
|
|
+ <el-image
|
|
|
+ v-if="previewImageList.length > 0"
|
|
|
+ :src="previewImageList[previewIndex]"
|
|
|
+ fit="contain"
|
|
|
+ class="preview-image-fullscreen"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 左箭头 - 上一张 -->
|
|
|
+ <div
|
|
|
+ v-if="previewIndex > 0"
|
|
|
+ class="arrow-left"
|
|
|
+ @click.stop="handlePrevImage"
|
|
|
+ >
|
|
|
+ <i class="el-icon-arrow-left" style="font-size: 40px"></i>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右箭头 - 下一张 -->
|
|
|
+ <div
|
|
|
+ v-if="previewIndex < previewImageList.length - 1"
|
|
|
+ class="arrow-right"
|
|
|
+ @click.stop="handleNextImage"
|
|
|
+ >
|
|
|
+ <i class="el-icon-arrow-right" style="font-size: 40px"></i>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 关闭按钮 -->
|
|
|
+ <div class="preview-close" @click.stop="showPreview = false">
|
|
|
+ <i class="el-icon-close" style="font-size: 24px"></i>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图片信息 -->
|
|
|
+ <div class="preview-info-fullscreen">
|
|
|
+ {{ previewIndex + 1 }} / {{ previewImageList.length }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'CameraDialogV2',
|
|
|
+ props: {
|
|
|
+ value: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ multiple: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ dialogVisible: false,
|
|
|
+ videoUrl: '',
|
|
|
+ videoLoading: true,
|
|
|
+ rotating: false,
|
|
|
+ capturing: false,
|
|
|
+ scanning: false,
|
|
|
+ readingIDCard: false,
|
|
|
+ scanningBarcode: false,
|
|
|
+ qrCodeResult: '',
|
|
|
+ idCardResult: null,
|
|
|
+ barcodeResult: '',
|
|
|
+ photoHistory: [],
|
|
|
+ showAllPhotos: false,
|
|
|
+ selectedPhotos: [],
|
|
|
+ previewImageList: [],
|
|
|
+ previewIndex: 0,
|
|
|
+ showPreview: false,
|
|
|
+ videoTimer: null,
|
|
|
+ port: '38088',
|
|
|
+ visible: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ apiBaseUrl() {
|
|
|
+ return 'http://127.0.0.1:' + this.port
|
|
|
+ },
|
|
|
+ displayedPhotos() {
|
|
|
+ return this.photoHistory.slice(0, 5)
|
|
|
+ },
|
|
|
+ idCardPreviewList() {
|
|
|
+ if (!this.idCardResult) return []
|
|
|
+ const list = []
|
|
|
+ if (this.idCardResult.photoBase64) {
|
|
|
+ list.push('data:image/jpeg;base64,' + this.idCardResult.photoBase64)
|
|
|
+ }
|
|
|
+ if (this.idCardResult.photoBase64_Z) {
|
|
|
+ list.push('data:image/jpeg;base64,' + this.idCardResult.photoBase64_Z)
|
|
|
+ }
|
|
|
+ if (this.idCardResult.photoBase64_F) {
|
|
|
+ list.push('data:image/jpeg;base64,' + this.idCardResult.photoBase64_F)
|
|
|
+ }
|
|
|
+ return list
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ value(newVal) {
|
|
|
+ this.dialogVisible = newVal
|
|
|
+ if (newVal) {
|
|
|
+ this.openVideo()
|
|
|
+ } else {
|
|
|
+ this.closeVideo()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ dialogVisible(newVal) {
|
|
|
+ this.$emit('input', newVal)
|
|
|
+ },
|
|
|
+ showPreview(newVal) {
|
|
|
+ if (newVal) {
|
|
|
+ document.addEventListener('keydown', this.handleKeydown)
|
|
|
+ } else {
|
|
|
+ document.removeEventListener('keydown', this.handleKeydown)
|
|
|
+ document.body.style.overflow = ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ changePort() {
|
|
|
+ this.visible = false
|
|
|
+ },
|
|
|
+ openVideo() {
|
|
|
+ this.videoLoading = true
|
|
|
+ this.videoUrl = `${this.apiBaseUrl}/video=stream&camidx=0?${Date.now()}`
|
|
|
+ },
|
|
|
+ handleVideoLoad() {
|
|
|
+ this.videoLoading = false
|
|
|
+ },
|
|
|
+ handleVideoError() {
|
|
|
+ this.videoLoading = false
|
|
|
+ // this.$message.error('摄像头加载失败,请检查设备连接')
|
|
|
+ },
|
|
|
+ closeVideo() {
|
|
|
+ if (this.videoUrl) {
|
|
|
+ const data = { camidx: '0' }
|
|
|
+ axios
|
|
|
+ .post(`${this.apiBaseUrl}/video=close`, JSON.stringify(data))
|
|
|
+ .catch(() => {
|
|
|
+ // 静默处理错误
|
|
|
+ })
|
|
|
+ this.videoUrl = ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async handleRotate(angle) {
|
|
|
+ this.rotating = true
|
|
|
+ try {
|
|
|
+ const data = {
|
|
|
+ camidx: '0',
|
|
|
+ rotate: String(angle)
|
|
|
+ }
|
|
|
+ const response = await axios.post(
|
|
|
+ `${this.apiBaseUrl}/video=rotate`,
|
|
|
+ JSON.stringify(data)
|
|
|
+ )
|
|
|
+ if (response.data) {
|
|
|
+ // this.$message.success(`旋转${angle}度成功`)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('旋转失败:' + (error.message || '未知错误'))
|
|
|
+ } finally {
|
|
|
+ this.rotating = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async handleCapture() {
|
|
|
+ this.capturing = true
|
|
|
+ try {
|
|
|
+ const data = {
|
|
|
+ filepath: 'base64',
|
|
|
+ rotate: '0',
|
|
|
+ cutpage: '0',
|
|
|
+ camidx: '0',
|
|
|
+ ColorMode: '0',
|
|
|
+ quality: '3'
|
|
|
+ }
|
|
|
+ const response = await axios.post(
|
|
|
+ `${this.apiBaseUrl}/video=grabimage`,
|
|
|
+ JSON.stringify(data)
|
|
|
+ )
|
|
|
+
|
|
|
+ if (response.data && response.data.photoBase64) {
|
|
|
+ const jpg = 'data:image/jpg;base64,' + response.data.photoBase64
|
|
|
+ // const pngBase64 = await this.convertToPng(response.data.photoBase64)
|
|
|
+ this.photoHistory.unshift(jpg)
|
|
|
+ if (this.multiple) {
|
|
|
+ this.selectedPhotos.unshift(jpg)
|
|
|
+ } else {
|
|
|
+ this.selectedPhotos = [jpg]
|
|
|
+ }
|
|
|
+ this.$emit('capture', jpg)
|
|
|
+ // this.$message.success('拍照成功')
|
|
|
+ } else {
|
|
|
+ this.$message.error('拍照失败:未获取到图片数据')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('拍照失败:' + (error.message || '未知错误'))
|
|
|
+ } finally {
|
|
|
+ this.capturing = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ convertToPng(base64String) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ try {
|
|
|
+ if (base64String.startsWith('data:image/png')) {
|
|
|
+ resolve(base64String)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const img = new Image()
|
|
|
+ img.onload = () => {
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+ canvas.width = img.width
|
|
|
+ canvas.height = img.height
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
+ ctx.drawImage(img, 0, 0)
|
|
|
+ const pngBase64 = canvas.toDataURL('image/png')
|
|
|
+ resolve(pngBase64)
|
|
|
+ }
|
|
|
+ img.onerror = () => {
|
|
|
+ reject(new Error('图片加载失败'))
|
|
|
+ }
|
|
|
+ if (!base64String.startsWith('data:')) {
|
|
|
+ img.src = 'data:image/jpeg;base64,' + base64String
|
|
|
+ } else {
|
|
|
+ img.src = base64String
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ reject(error)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ handleClearAll() {
|
|
|
+ this.photoHistory = []
|
|
|
+ this.selectedPhotos = []
|
|
|
+ this.showAllPhotos = false
|
|
|
+ this.$message.success('已清空全部拍照记录')
|
|
|
+ },
|
|
|
+ isPhotoSelected(photo) {
|
|
|
+ return this.selectedPhotos.includes(photo)
|
|
|
+ },
|
|
|
+ handleSelectPhoto(photo) {
|
|
|
+ const index = this.selectedPhotos.indexOf(photo)
|
|
|
+ if (index > -1) {
|
|
|
+ // 如果已选中,则取消选择
|
|
|
+ this.selectedPhotos.splice(index, 1)
|
|
|
+ } else {
|
|
|
+ // 如果未选中
|
|
|
+ if (this.multiple) {
|
|
|
+ // 多选模式:直接添加
|
|
|
+ this.selectedPhotos.push(photo)
|
|
|
+ } else {
|
|
|
+ // 单选模式:清空之前的选择,只选择当前照片
|
|
|
+ this.selectedPhotos = [photo]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handlePreviewPhoto(photo, index) {
|
|
|
+ this.previewImageList = this.photoHistory
|
|
|
+ this.previewIndex = index
|
|
|
+ this.showPreview = true
|
|
|
+ document.body.style.overflow = 'hidden'
|
|
|
+ },
|
|
|
+ handlePrevImage() {
|
|
|
+ if (this.previewIndex > 0) {
|
|
|
+ this.previewIndex--
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleNextImage() {
|
|
|
+ if (this.previewIndex < this.previewImageList.length - 1) {
|
|
|
+ this.previewIndex++
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleKeydown(event) {
|
|
|
+ if (!this.showPreview) return
|
|
|
+ if (event.key === 'ArrowLeft') {
|
|
|
+ this.handlePrevImage()
|
|
|
+ } else if (event.key === 'ArrowRight') {
|
|
|
+ this.handleNextImage()
|
|
|
+ } else if (event.key === 'Escape') {
|
|
|
+ this.showPreview = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleConfirm() {
|
|
|
+ if (this.selectedPhotos.length === 0) {
|
|
|
+ this.$message.warning('请选择至少一张图片')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.$emit('confirm', this.selectedPhotos)
|
|
|
+ this.dialogVisible = false
|
|
|
+ this.showAllPhotos = false
|
|
|
+ },
|
|
|
+ handleClose() {
|
|
|
+ this.dialogVisible = false
|
|
|
+ this.closeVideo()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeUnmount() {
|
|
|
+ this.closeVideo()
|
|
|
+ if (this.videoTimer) {
|
|
|
+ clearInterval(this.videoTimer)
|
|
|
+ }
|
|
|
+ document.removeEventListener('keydown', this.handleKeydown)
|
|
|
+ document.body.style.overflow = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.camera-dialog-content {
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.video-preview {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ background-color: #000;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ min-height: 400px;
|
|
|
+ height: 400px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.video-stream {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 400px;
|
|
|
+ height: auto;
|
|
|
+ width: auto;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+.button-group {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-history {
|
|
|
+ margin-top: 20px;
|
|
|
+ min-height: 140px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-history-empty {
|
|
|
+ min-height: 140px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background-color: #fafafa;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px dashed #dcdfe6;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-tip {
|
|
|
+ margin-top: 10px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.thumbnail-container {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10px;
|
|
|
+ justify-content: flex-start;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.thumbnail-item {
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 2px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: all 0.3s;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.thumbnail-item:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+ transform: scale(1.05);
|
|
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.thumbnail-item.selected {
|
|
|
+ border-color: #409eff;
|
|
|
+ border-width: 3px;
|
|
|
+ box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.selected-badge {
|
|
|
+ position: absolute;
|
|
|
+ top: 5px;
|
|
|
+ right: 5px;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ background-color: #409eff;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: white;
|
|
|
+ font-size: 14px;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.thumbnail-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.more-button-container {
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.more-button {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.all-photos-container {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
|
+ gap: 15px;
|
|
|
+ max-height: 60vh;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.all-photo-item {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ aspect-ratio: 1;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 2px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.all-photo-item:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+ transform: scale(1.05);
|
|
|
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.all-photo-item.selected {
|
|
|
+ border-color: #409eff;
|
|
|
+ border-width: 3px;
|
|
|
+ box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.selected-badge-large {
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ right: 10px;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ background-color: #409eff;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: white;
|
|
|
+ font-size: 18px;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.selected-info {
|
|
|
+ margin-top: 15px;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f0f9ff;
|
|
|
+ border: 1px solid #409eff;
|
|
|
+ border-radius: 4px;
|
|
|
+ text-align: center;
|
|
|
+ color: #409eff;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.qr-result {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.qr-content {
|
|
|
+ padding: 10px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.qr-text {
|
|
|
+ word-break: break-all;
|
|
|
+ margin: 10px 0;
|
|
|
+ padding: 8px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-family: monospace;
|
|
|
+}
|
|
|
+
|
|
|
+.idcard-result {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.idcard-card {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.idcard-content {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.idcard-photo {
|
|
|
+ flex: 0 0 200px;
|
|
|
+}
|
|
|
+
|
|
|
+.idcard-image {
|
|
|
+ width: 200px;
|
|
|
+ height: auto;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.idcard-info {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.idcard-actions {
|
|
|
+ margin-top: 15px;
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.barcode-result {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.barcode-content {
|
|
|
+ padding: 10px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.barcode-text {
|
|
|
+ word-break: break-all;
|
|
|
+ margin: 10px 0;
|
|
|
+ padding: 8px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-family: monospace;
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-preview {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ z-index: 8888;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background-color: rgba(0, 0, 0, 0.9);
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-content {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-image-fullscreen {
|
|
|
+ max-width: 90%;
|
|
|
+ max-height: 90%;
|
|
|
+ cursor: default;
|
|
|
+}
|
|
|
+
|
|
|
+.arrow-left,
|
|
|
+.arrow-right {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ background-color: rgba(255, 255, 255, 0.2);
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ color: white;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.arrow-left:hover,
|
|
|
+.arrow-right:hover {
|
|
|
+ background-color: rgba(255, 255, 255, 0.4);
|
|
|
+ transform: translateY(-50%) scale(1.1);
|
|
|
+}
|
|
|
+
|
|
|
+.arrow-left {
|
|
|
+ left: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.arrow-right {
|
|
|
+ right: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-close {
|
|
|
+ position: absolute;
|
|
|
+ top: 30px;
|
|
|
+ right: 30px;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ background-color: rgba(255, 255, 255, 0.2);
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ color: white;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-close:hover {
|
|
|
+ background-color: rgba(255, 255, 255, 0.4);
|
|
|
+ transform: scale(1.1);
|
|
|
+}
|
|
|
+
|
|
|
+.preview-info-fullscreen {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 30px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ padding: 10px 20px;
|
|
|
+ background-color: rgba(0, 0, 0, 0.6);
|
|
|
+ color: white;
|
|
|
+ border-radius: 20px;
|
|
|
+ font-size: 16px;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.all-photo-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-index {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
|
|
|
+ color: white;
|
|
|
+ padding: 5px;
|
|
|
+ font-size: 12px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+</style>
|
|
|
+<style lang="scss">
|
|
|
+.attachment-uploader-dialog {
|
|
|
+ .camera-dialog {
|
|
|
+ .el-dialog__body {
|
|
|
+ height: auto !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|