图片上传优化版

This commit is contained in:
WindowBird 2025-08-25 16:26:57 +08:00
parent 5fd9b217ef
commit df1709deab
2 changed files with 369 additions and 292 deletions

View File

@ -0,0 +1,347 @@
<template>
<view class="image-uploader">
<!-- 上传区域 -->
<view class="upload-area" @click="chooseImage">
<view v-if="!imageUrl" class="upload-placeholder">
<text class="upload-icon">📷</text>
<!-- <text class="upload-text">{{ placeholder || '点击选择图片' }}</text>-->
<!-- <text class="upload-hint">{{ hint || '支持 JPG、PNG 格式' }}</text>-->
</view>
<image v-else :src="imageUrl" class="preview-image" mode="aspectFit"></image>
</view>
<!-- 错误提示 -->
<view v-if="errorMessage" class="error-message">
<text class="error-text">{{ errorMessage }}</text>
</view>
</view>
</template>
<script>
import { getQiniuUploadToken, uploadToQiniu } from '@/api/upload.js'
export default {
name: 'ImageUploader',
props: {
//
placeholder: {
type: String,
default: '点击选择图片',
},
//
hint: {
type: String,
default: '支持 JPG、PNG 格式',
},
//
showDelete: {
type: Boolean,
default: true,
},
//
height: {
type: String,
default: '400rpx',
},
width: {
type: String,
default: '400rpx',
},
//
autoUpload: {
type: Boolean,
default: true,
},
// MB
maxSize: {
type: Number,
default: 10,
},
},
data() {
return {
selectedImagePath: '', //
imageUrl: '', // URL
uploadResult: '', // URL
uploading: false, //
errorMessage: '', //
qiniuToken: '', // token
}
},
mounted() {
this.getQiniuToken()
},
methods: {
//
chooseImage() {
if (this.uploading) return
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: res => {
const filePath = res.tempFilePaths[0]
//
if (!this.checkFileSize(filePath)) {
return
}
this.selectedImagePath = filePath
this.imageUrl = filePath
this.uploadResult = ''
this.errorMessage = ''
//
if (this.autoUpload) {
this.uploadImage()
}
//
this.$emit('select', {
filePath,
size: res.tempFiles[0].size,
})
},
fail: err => {
console.error('选择图片失败:', err)
this.errorMessage = '选择图片失败'
this.$emit('error', err)
},
})
},
//
checkFileSize(filePath) {
return new Promise(resolve => {
uni.getFileInfo({
filePath,
success: res => {
const sizeInMB = res.size / (1024 * 1024)
if (sizeInMB > this.maxSize) {
this.errorMessage = `文件大小不能超过 ${this.maxSize}MB`
uni.showToast({
title: `文件大小不能超过 ${this.maxSize}MB`,
icon: 'none',
})
resolve(false)
} else {
resolve(true)
}
},
fail: () => {
resolve(true) //
},
})
})
},
//
async uploadImage() {
if (!this.selectedImagePath) {
this.errorMessage = '请先选择图片'
return
}
this.uploading = true
this.errorMessage = ''
try {
// token
if (!this.qiniuToken) {
await this.getQiniuToken()
}
//
const key = `uploads/${Date.now()}_${Math.random().toString(36).slice(2)}.jpg`
//
const result = await uploadToQiniu(this.selectedImagePath, this.qiniuToken, key)
// URL
this.uploadResult = `https://api.ccttiot.com/${result.key}`
//
this.$emit('success', {
url: this.uploadResult,
key: result.key,
originalPath: this.selectedImagePath,
})
//
uni.showToast({
title: '上传成功',
icon: 'success',
})
} catch (error) {
console.error('上传失败:', error)
this.errorMessage = error.message || '上传失败'
this.$emit('error', error)
} finally {
this.uploading = false
}
},
//
deleteImage() {
uni.showModal({
title: '确认删除',
content: '确定要删除这张图片吗?',
success: res => {
if (res.confirm) {
this.selectedImagePath = ''
this.imageUrl = ''
this.uploadResult = ''
this.errorMessage = ''
this.$emit('delete')
}
},
})
},
// token
async getQiniuToken() {
try {
const res = await getQiniuUploadToken()
console.log('Token响应:', res)
if (res.code === 200) {
//
const token = res.data
if (token) {
this.qiniuToken = token
console.log('Token获取成功:', token.substring(0, 20) + '...')
} else {
throw new Error('Token字段不存在')
}
} else {
throw new Error(res.msg || '获取Token失败')
}
} catch (error) {
console.error('获取Token失败:', error)
this.errorMessage = `获取上传凭证失败: ${error.message}`
this.$emit('error', error)
}
},
//
manualUpload() {
if (!this.autoUpload) {
this.uploadImage()
}
},
//
getUploadResult() {
return {
url: this.uploadResult,
localPath: this.selectedImagePath,
isUploaded: !!this.uploadResult,
}
},
//
reset() {
this.selectedImagePath = ''
this.imageUrl = ''
this.uploadResult = ''
this.uploading = false
this.errorMessage = ''
},
},
}
</script>
<style lang="scss" scoped>
.image-uploader {
.upload-area {
width: v-bind(width);
height: v-bind(height);
background: #fff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx dashed #ddd;
position: relative;
overflow: hidden;
.upload-placeholder {
text-align: center;
.upload-icon {
display: block;
font-size: 60rpx;
margin-bottom: 20rpx;
}
.upload-text {
display: block;
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.upload-hint {
display: block;
font-size: 24rpx;
color: #999;
}
}
.preview-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
.delete-btn {
position: absolute;
top: 16rpx;
right: 16rpx;
width: 60rpx;
height: 60rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.delete-icon {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
}
}
.upload-status {
margin-top: 16rpx;
text-align: center;
.status-text {
font-size: 26rpx;
color: #666;
}
}
.error-message {
margin-top: 16rpx;
padding: 16rpx;
background: #fff2f0;
border: 1rpx solid #ffccc7;
border-radius: 8rpx;
.error-text {
font-size: 26rpx;
color: #ff4d4f;
line-height: 1.4;
}
}
}
</style>

View File

@ -1,186 +1,38 @@
<template>
<view class="image-upload-page">
<view class="content">
<!-- 上传区域 -->
<view class="upload-area" @click="chooseImage">
<view v-if="!imageUrl" class="upload-placeholder">
<u-icon color="#ccc" name="camera" size="60"></u-icon>
<text class="upload-text">点击选择图片</text>
<text class="upload-hint">支持 JPGPNG 格式</text>
</view>
<image v-else :src="imageUrl" class="preview-image" mode="aspectFit"></image>
</view>
<!-- 上传按钮 -->
<button
:disabled="!selectedImagePath || uploading"
:loading="uploading"
class="upload-btn"
@click="uploadImage"
>
{{ uploading ? '上传中...' : '确认上传' }}
</button>
<!-- 结果展示 -->
<view v-if="uploadResult" class="result-section">
<view class="result-item">
<text class="result-label">上传状态:</text>
<text class="result-value success">成功</text>
</view>
<view class="result-item">
<text class="result-label">图片URL:</text>
<text class="result-url" @click="copyUrl">{{ uploadResult }}</text>
</view>
<button class="copy-btn" @click="copyUrl">复制URL</button>
</view>
<!-- 错误信息 -->
<view v-if="errorMessage" class="error-section">
<text class="error-text">{{ errorMessage }}</text>
</view>
<!-- 使用图片上传组件 -->
<image-uploader
ref="uploader"
:height="'150rpx'"
:width="'150rpx'"
@error="handleUploadError"
@success="handleUploadSuccess"
/>
</view>
</view>
</template>
<script>
import { getQiniuUploadToken, uploadToQiniu } from '@/api/upload.js'
export default {
data() {
return {
selectedImagePath: '', //
imageUrl: '', // URL
uploadResult: '', // URL
uploading: false, //
errorMessage: '', //
qiniuToken: '', // token
}
},
onLoad() {
this.getQiniuToken()
},
methods: {
//
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: res => {
this.selectedImagePath = res.tempFilePaths[0]
this.imageUrl = res.tempFilePaths[0]
this.uploadResult = ''
this.errorMessage = ''
},
fail: err => {
console.error('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none',
})
},
//
handleUploadSuccess(result) {
console.log('图片上传成功:', result.url)
uni.showToast({
title: '上传成功',
icon: 'success',
})
},
//
async uploadImage() {
if (!this.selectedImagePath) {
uni.showToast({
title: '请先选择图片',
icon: 'none',
})
return
}
this.uploading = true
this.errorMessage = ''
try {
// token
if (!this.qiniuToken) {
await this.getQiniuToken()
}
//
const key = `uploads/${Date.now()}_${Math.random().toString(36).slice(2)}.jpg`
//
const result = await uploadToQiniu(this.selectedImagePath, this.qiniuToken, key)
// URL
this.uploadResult = `https://api.ccttiot.com/${result.key}`
//
console.log('触发图片上传成功事件:', this.uploadResult)
uni.$emit('image-upload-success', this.uploadResult)
//
uni.showToast({
title: '上传成功',
icon: 'success',
})
//
setTimeout(() => {
uni.navigateBack({
delta: 1,
})
}, 500)
} catch (error) {
console.error('上传失败:', error)
this.errorMessage = error.message || '上传失败'
uni.showToast({
title: '上传失败',
icon: 'none',
})
} finally {
this.uploading = false
}
},
// token
async getQiniuToken() {
try {
const res = await getQiniuUploadToken()
console.log('Token响应:', res)
if (res.code === 200) {
//
const token = res.data?.token || res.data?.uploadToken || res.token || res.data
if (token) {
this.qiniuToken = token
console.log('Token获取成功:', token.substring(0, 20) + '...')
} else {
throw new Error('Token字段不存在')
}
} else {
throw new Error(res.msg || '获取Token失败')
}
} catch (error) {
console.error('获取Token失败:', error)
this.errorMessage = `获取上传凭证失败: ${error.message}`
uni.showToast({
title: '获取上传凭证失败',
icon: 'none',
})
}
},
// URL
copyUrl() {
if (this.uploadResult) {
uni.setClipboardData({
data: this.uploadResult,
success: () => {
uni.showToast({
title: 'URL已复制',
icon: 'success',
})
},
})
}
//
handleUploadError(error) {
console.error('图片上传失败:', error)
uni.showToast({
title: '上传失败',
icon: 'none',
})
},
},
}
@ -194,127 +46,5 @@ export default {
.content {
padding: 30rpx;
}
.upload-area {
width: 100%;
height: 400rpx;
background: #fff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
border: 2rpx dashed #ddd;
.upload-placeholder {
text-align: center;
.upload-text {
display: block;
font-size: 32rpx;
color: #333;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.upload-hint {
display: block;
font-size: 24rpx;
color: #999;
}
}
.preview-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
.upload-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #f15a04;
color: #fff;
border-radius: 12rpx;
font-size: 32rpx;
border: none;
margin-bottom: 30rpx;
&:active {
background: #ff5e00;
}
&:disabled {
background: #ccc;
color: #999;
}
}
.result-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
.result-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.result-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-right: 20rpx;
min-width: 120rpx;
}
.result-value {
font-size: 28rpx;
&.success {
color: #52c41a;
}
}
.result-url {
flex: 1;
font-size: 24rpx;
color: #007aff;
word-break: break-all;
line-height: 1.4;
}
}
.copy-btn {
width: 100%;
height: 70rpx;
line-height: 70rpx;
background: #f0f0f0;
color: #333;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
&:active {
background: #e0e0e0;
}
}
}
.error-section {
background: #fff2f0;
border: 1rpx solid #ffccc7;
border-radius: 12rpx;
padding: 20rpx;
.error-text {
font-size: 26rpx;
color: #ff4d4f;
line-height: 1.4;
}
}
}
</style>