This commit is contained in:
aaron 2025-03-07 14:54:17 +08:00
parent c5b04fbcb8
commit b94f2935a3
2 changed files with 448 additions and 0 deletions

View File

@ -0,0 +1,439 @@
<template>
<div class="image-recognition-container">
<div class="recognition-card">
<h1 class="page-title">快递取件码识别</h1>
<div class="upload-section">
<div
class="image-upload-area"
@click="triggerFileInput"
:class="{ 'has-image': previewImage }"
>
<div v-if="!previewImage" class="upload-placeholder">
<div class="upload-icon">
<i class="el-icon-upload"></i>
</div>
<div class="upload-text">点击上传图片</div>
<div class="upload-hint">支持快递短信截图取件码截图等</div>
</div>
<img v-else :src="previewImage" class="preview-image" alt="预览图片" />
<input
type="file"
ref="fileInput"
accept="image/*"
class="file-input"
@change="handleFileChange"
/>
</div>
<div class="action-buttons">
<button
class="btn-upload"
@click="triggerFileInput"
:disabled="isRecognizing"
>
{{ previewImage ? '重新选择' : '选择图片' }}
</button>
<button
class="btn-recognize"
@click="recognizeImage"
:disabled="!previewImage || isRecognizing"
>
{{ isRecognizing ? 'AI识别中...' : '开始识别' }}
</button>
</div>
</div>
<div v-if="recognitionStatus" class="status-message" :class="statusClass">
{{ recognitionStatus }}
</div>
<div v-if="recognitionResult" class="result-section">
<h2 class="result-title">识别结果</h2>
<div class="result-content">
<pre class="formatted-result">{{ recognitionResult.formatted_text }}</pre>
<div class="result-details">
<div v-for="(station, index) in recognitionResult.raw.stations" :key="index" class="station-item">
<div class="station-name">驿站{{ station.name }}</div>
<div v-for="(code, codeIndex) in station.pickup_codes" :key="codeIndex" class="pickup-code">
取件码<span class="code-value">{{ code }}</span>
<button class="btn-copy" @click="copyToClipboard(code)">复制</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import apiClient from '../services/api'
export default {
name: 'ImageRecognition',
setup() {
const fileInput = ref(null)
const previewImage = ref(null)
const selectedFile = ref(null)
const isRecognizing = ref(false)
const recognitionStatus = ref('')
const statusClass = ref('')
const recognitionResult = ref(null)
//
const triggerFileInput = () => {
if (!isRecognizing.value) {
fileInput.value.click()
}
}
//
const handleFileChange = (event) => {
const file = event.target.files[0]
if (file) {
selectedFile.value = file
//
const reader = new FileReader()
reader.onload = (e) => {
previewImage.value = e.target.result
}
reader.readAsDataURL(file)
//
recognitionStatus.value = ''
recognitionResult.value = null
}
}
//
const recognizeImage = async () => {
if (!selectedFile.value || isRecognizing.value) return
isRecognizing.value = true
recognitionStatus.value = 'AI 识别中...'
statusClass.value = 'status-processing'
try {
const formData = new FormData()
formData.append('file', selectedFile.value)
const response = await apiClient.post('/api/ai/extract_pickup_code', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data && response.data.code === 200) {
recognitionResult.value = response.data.data
recognitionStatus.value = '识别成功!'
statusClass.value = 'status-success'
} else {
recognitionStatus.value = response.data?.message || '识别失败,请重试'
statusClass.value = 'status-error'
}
} catch (error) {
console.error('识别失败:', error)
recognitionStatus.value = '识别失败,请重试'
statusClass.value = 'status-error'
} finally {
isRecognizing.value = false
}
}
//
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
.then(() => {
alert('取件码已复制到剪贴板')
})
.catch(err => {
console.error('复制失败:', err)
alert('复制失败,请手动复制')
})
}
onMounted(() => {
document.title = '快递取件码识别'
})
onBeforeUnmount(() => {
document.title = '蜂快到家'
})
return {
fileInput,
previewImage,
isRecognizing,
recognitionStatus,
statusClass,
recognitionResult,
triggerFileInput,
handleFileChange,
recognizeImage,
copyToClipboard
}
}
}
</script>
<style scoped>
.image-recognition-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 15px;
background: linear-gradient(to bottom, #FFCC00, #FFFFFF);
}
.recognition-card {
width: 100%;
max-width: 500px;
background-color: #FFFFFF;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 25px 20px;
margin-bottom: 20px;
}
.page-title {
font-size: 22px;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 25px;
}
.upload-section {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 20px;
}
.image-upload-area {
width: 100%;
height: 200px;
border: 2px dashed #DDDDDD;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.image-upload-area:hover {
border-color: #FFCC00;
background-color: rgba(255, 204, 0, 0.05);
}
.image-upload-area.has-image {
border-style: solid;
border-color: #FFCC00;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.upload-icon {
font-size: 40px;
color: #DDDDDD;
}
.upload-text {
font-size: 16px;
font-weight: 500;
color: #666666;
}
.upload-hint {
font-size: 12px;
color: #999999;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.file-input {
display: none;
}
.action-buttons {
display: flex;
gap: 15px;
}
.btn-upload, .btn-recognize {
flex: 1;
padding: 12px 15px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
text-align: center;
border: none;
}
.btn-upload {
background-color: #F2F2F2;
color: #666666;
}
.btn-recognize {
background-color: #FFCC00;
color: #333333;
}
.btn-upload:hover {
background-color: #E6E6E6;
}
.btn-recognize:hover {
background-color: #FFD633;
}
.btn-upload:disabled, .btn-recognize:disabled {
background-color: #DDDDDD;
color: #999999;
cursor: not-allowed;
}
.status-message {
padding: 10px 15px;
border-radius: 8px;
font-size: 14px;
margin-bottom: 20px;
text-align: center;
}
.status-processing {
background-color: rgba(255, 204, 0, 0.1);
color: #FFCC00;
border: 1px solid rgba(255, 204, 0, 0.3);
}
.status-success {
background-color: rgba(0, 230, 118, 0.1);
color: #00E676;
border: 1px solid rgba(0, 230, 118, 0.3);
}
.status-error {
background-color: rgba(255, 87, 87, 0.1);
color: #FF5757;
border: 1px solid rgba(255, 87, 87, 0.3);
}
.result-section {
border-top: 1px solid #EEEEEE;
padding-top: 20px;
}
.result-title {
font-size: 18px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.result-content {
background-color: #F9F9F9;
border-radius: 8px;
padding: 15px;
}
.formatted-result {
font-family: monospace;
white-space: pre-wrap;
margin-bottom: 15px;
padding: 10px;
background-color: #F2F2F2;
border-radius: 6px;
font-size: 14px;
color: #333333;
}
.result-details {
display: flex;
flex-direction: column;
gap: 15px;
}
.station-item {
padding: 10px;
background-color: #FFFFFF;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.station-name {
font-weight: 500;
color: #333333;
margin-bottom: 8px;
}
.pickup-code {
display: flex;
align-items: center;
gap: 10px;
color: #666666;
}
.code-value {
font-weight: bold;
color: #FFCC00;
font-size: 18px;
}
.btn-copy {
padding: 4px 8px;
background-color: #FFCC00;
color: #333333;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.3s;
}
.btn-copy:hover {
background-color: #FFD633;
}
@media (max-width: 480px) {
.recognition-card {
padding: 20px 15px;
}
.page-title {
font-size: 20px;
margin-bottom: 20px;
}
.image-upload-area {
height: 180px;
}
.btn-upload, .btn-recognize {
padding: 10px 12px;
font-size: 15px;
}
}
</style>

View File

@ -6,6 +6,7 @@ import Dashboard from '../components/Dashboard.vue'
import HowToUse from '../components/HowToUse.vue' import HowToUse from '../components/HowToUse.vue'
import CommunityRequest from '../components/CommunityRequest.vue' import CommunityRequest from '../components/CommunityRequest.vue'
import PartnerRequest from '../components/PartnerRequest.vue' import PartnerRequest from '../components/PartnerRequest.vue'
import ImageRecognition from '../components/ImageRecognition.vue'
const routes = [ const routes = [
{ {
@ -60,6 +61,14 @@ const routes = [
meta: { meta: {
title: '申请成为服务商/运营商' title: '申请成为服务商/运营商'
} }
},
{
path: '/image-recognition',
name: 'ImageRecognition',
component: ImageRecognition,
meta: {
title: '快递取件码识别'
}
} }
] ]