图片上传七牛云成功

This commit is contained in:
WindowBird 2025-08-21 13:37:29 +08:00
parent 523d86d6e9
commit 6e73e36644
7 changed files with 1104 additions and 30 deletions

189
README_IMAGE_UPLOAD.md Normal file
View File

@ -0,0 +1,189 @@
# 单图上传功能说明
## 功能概述
本项目提供了完整的单图上传功能支持选择图片、上传到七牛云、返回图片URL。
## 文件结构
```
├── api/upload.js # 上传相关API
├── pages/image-upload/ # 上传页面
│ └── image-upload.vue
├── pages/image-upload-demo/ # 演示页面
│ └── image-upload-demo.vue
└── examples/ # 使用示例
└── single-image-upload-usage.vue
```
## 核心功能
### 1. API接口 (`api/upload.js`)
#### 获取七牛云上传Token
```javascript
import { getQiniuUploadToken } from '@/api/upload.js'
const res = await getQiniuUploadToken()
if (res.code === 200) {
const token = res.data?.token || res.data?.uploadToken || res.token || res.data
}
```
#### 上传图片到七牛云
```javascript
import { uploadToQiniu } from '@/api/upload.js'
const result = await uploadToQiniu(filePath, token, key)
const imageUrl = `https://api.ccttiot.com/${result.key}`
```
### 2. 上传页面 (`pages/image-upload/image-upload.vue`)
完整的图片上传页面,包含:
- 图片选择功能
- 预览功能
- 上传进度显示
- 错误处理
- 结果展示
## 使用方法
### 方法一:跳转到上传页面
```javascript
// 跳转到上传页面
uni.navigateTo({
url: '/pages/image-upload/image-upload'
})
// 监听上传成功事件
uni.$on('image-upload-success', (imageUrl) => {
console.log('上传成功:', imageUrl)
})
```
### 方法二:内嵌上传功能
```javascript
import { getQiniuUploadToken, uploadToQiniu } from '@/api/upload.js'
export default {
data() {
return {
selectedImage: '',
qiniuToken: '',
uploading: false
}
},
methods: {
// 选择图片
chooseImage() {
uni.chooseImage({
count: 1,
success: (res) => {
this.selectedImage = res.tempFilePaths[0]
}
})
},
// 上传图片
async uploadImage() {
if (!this.selectedImage) return
this.uploading = true
try {
// 获取token
if (!this.qiniuToken) {
const res = await getQiniuUploadToken()
this.qiniuToken = res.data?.token || res.data?.uploadToken || res.token || res.data
}
// 生成文件名
const key = `uploads/${Date.now()}_${Math.random().toString(36).slice(2)}.jpg`
// 上传
const result = await uploadToQiniu(this.selectedImage, this.qiniuToken, key)
const imageUrl = `https://api.ccttiot.com/${result.key}`
console.log('上传成功:', imageUrl)
} catch (error) {
console.error('上传失败:', error)
} finally {
this.uploading = false
}
}
}
}
```
## 页面配置
确保在 `pages.json` 中注册了相关页面:
```json
{
"pages": [
{
"path": "pages/image-upload/image-upload",
"style": {
"navigationBarTitleText": "图片上传",
"navigationStyle": "custom"
}
},
{
"path": "pages/image-upload-demo/image-upload-demo",
"style": {
"navigationBarTitleText": "图片上传演示",
"navigationStyle": "custom"
}
}
]
}
```
## 功能特点
### ✅ 已实现功能
- [x] 图片选择和预览
- [x] 七牛云上传
- [x] 上传进度显示
- [x] 错误处理和提示
- [x] 结果URL展示
- [x] URL复制功能
- [x] 多种使用方式
- [x] 完整的示例代码
### 🔧 技术特性
- 支持相册和相机选择
- 自动获取七牛云上传Token
- 智能Token字段解析
- 唯一文件名生成
- 完整的错误处理
- 响应式UI设计
## 注意事项
1. **Token获取**确保后端API `/common/qiniu/uploadInfo` 正常工作
2. **域名配置**:七牛云上传域名已配置为 `https://up-z2.qiniup.com`
3. **图片域名**:图片访问域名配置为 `https://api.ccttiot.com`
4. **文件格式**:支持 JPG、PNG 等常见图片格式
5. **文件大小**:建议图片大小不超过 5MB
## 调试
如果遇到上传问题,可以:
1. 检查控制台日志
2. 验证Token获取是否成功
3. 确认网络连接正常
4. 检查七牛云配置是否正确
## 示例页面
访问以下页面查看完整示例:
- `/pages/image-upload-demo/image-upload-demo` - 演示页面
- `/examples/single-image-upload-usage.vue` - 使用示例

44
api/upload.js Normal file
View File

@ -0,0 +1,44 @@
import request from '@/utils/request.js'
/**
* 获取七牛云上传token
* @returns {Promise}
*/
export function getQiniuUploadToken() {
return request({
url: '/common/qiniuToken',
method: 'GET'
})
}
/**
* 上传图片到七牛云
* @param {string} filePath - 文件路径
* @param {string} token - 七牛云上传token
* @param {string} key - 文件key
* @returns {Promise}
*/
export function uploadToQiniu(filePath, token, key) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'https://up-z2.qiniup.com',
filePath: filePath,
name: 'file',
formData: {
token: token,
key: key,
},
success: (res) => {
try {
const data = JSON.parse(res.data)
resolve(data)
} catch (error) {
reject(new Error('响应数据解析失败'))
}
},
fail: (error) => {
reject(error)
},
})
})
}

View File

@ -1 +0,0 @@

View File

@ -0,0 +1,318 @@
<template>
<view class="usage-example">
<view class="example-header">
<text class="title">单图上传使用示例</text>
<text class="desc">展示如何在页面中集成图片上传功能</text>
</view>
<!-- 方法一跳转到上传页面 -->
<view class="method-section">
<text class="method-title">方法一跳转到上传页面</text>
<text class="method-desc">跳转到专门的上传页面上传完成后返回</text>
<button class="method-btn" @click="method1">跳转上传</button>
<view v-if="method1Result" class="result-box">
<text class="result-label">上传结果</text>
<image :src="method1Result" mode="widthFix" class="result-image"></image>
<text class="result-url">{{ method1Result }}</text>
</view>
</view>
<!-- 方法二内嵌上传组件 -->
<view class="method-section">
<text class="method-title">方法二内嵌上传组件</text>
<text class="method-desc">在当前页面直接使用上传组件</text>
<view class="inline-upload">
<view class="upload-area" @click="chooseImage">
<view v-if="!selectedImage" class="upload-placeholder">
<text>点击选择图片</text>
</view>
<image v-else :src="selectedImage" mode="aspectFit" class="preview-image"></image>
</view>
<button
class="upload-btn"
@click="uploadSelectedImage"
:disabled="!selectedImage || uploading"
>
{{ uploading ? '上传中...' : '上传' }}
</button>
</view>
<view v-if="method2Result" class="result-box">
<text class="result-label">上传结果</text>
<image :src="method2Result" mode="widthFix" class="result-image"></image>
<text class="result-url">{{ method2Result }}</text>
</view>
</view>
</view>
</template>
<script>
import { getQiniuUploadToken, uploadToQiniu } from '@/api/upload.js'
export default {
data() {
return {
//
method1Result: '',
//
selectedImage: '',
uploading: false,
method2Result: '',
qiniuToken: '',
}
},
onLoad() {
//
uni.$on('image-upload-success', this.onImageUploadSuccess)
},
onUnload() {
//
uni.$off('image-upload-success', this.onImageUploadSuccess)
},
methods: {
//
method1() {
uni.navigateTo({
url: '/pages/image-upload/image-upload'
})
},
//
onImageUploadSuccess(imageUrl) {
this.method1Result = imageUrl
uni.showToast({
title: '图片上传成功',
icon: 'success'
})
},
//
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.selectedImage = res.tempFilePaths[0]
this.method2Result = ''
}
})
},
//
async uploadSelectedImage() {
if (!this.selectedImage) {
uni.showToast({
title: '请先选择图片',
icon: 'none'
})
return
}
this.uploading = true
try {
// token
if (!this.qiniuToken) {
await this.getQiniuToken()
}
//
const key = `examples/${Date.now()}_${Math.random().toString(36).slice(2)}.jpg`
//
const result = await uploadToQiniu(this.selectedImage, this.qiniuToken, key)
this.method2Result = `https://api.ccttiot.com/${result.key}`
uni.showToast({
title: '上传成功',
icon: 'success'
})
} catch (error) {
console.error('上传失败:', error)
uni.showToast({
title: '上传失败',
icon: 'none'
})
} finally {
this.uploading = false
}
},
// token
async getQiniuToken() {
try {
const res = await getQiniuUploadToken()
if (res.code === 200) {
const token = res.data?.token || res.data?.uploadToken || res.token || res.data
if (token) {
this.qiniuToken = token
} else {
throw new Error('Token字段不存在')
}
} else {
throw new Error(res.msg || '获取Token失败')
}
} catch (error) {
console.error('获取Token失败:', error)
throw error
}
}
}
}
</script>
<style lang="scss" scoped>
.usage-example {
padding: 30rpx;
background: #f5f5f5;
min-height: 100vh;
.example-header {
text-align: center;
margin-bottom: 40rpx;
.title {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.desc {
display: block;
font-size: 26rpx;
color: #666;
}
}
.method-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
.method-title {
display: block;
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.method-desc {
display: block;
font-size: 24rpx;
color: #666;
margin-bottom: 20rpx;
}
.method-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background: #007aff;
color: #fff;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
margin-bottom: 20rpx;
&:active {
background: #0056cc;
}
}
.inline-upload {
margin-bottom: 20rpx;
.upload-area {
width: 100%;
height: 200rpx;
background: #f8f8f8;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
border: 2rpx dashed #ddd;
.upload-placeholder {
text-align: center;
text {
font-size: 28rpx;
color: #999;
}
}
.preview-image {
width: 100%;
height: 100%;
border-radius: 12rpx;
}
}
.upload-btn {
width: 100%;
height: 70rpx;
line-height: 70rpx;
background: #52c41a;
color: #fff;
border-radius: 8rpx;
font-size: 26rpx;
border: none;
&:active {
background: #389e0d;
}
&:disabled {
background: #ccc;
color: #999;
}
}
}
.result-box {
border-top: 1rpx solid #eee;
padding-top: 20rpx;
.result-label {
display: block;
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
}
.result-image {
width: 100%;
border-radius: 8rpx;
margin-bottom: 15rpx;
}
.result-url {
display: block;
font-size: 22rpx;
color: #007aff;
word-break: break-all;
line-height: 1.4;
background: #f8f8f8;
padding: 15rpx;
border-radius: 6rpx;
}
}
}
}
</style>

View File

@ -1,17 +1,17 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "设备租赁",
"navigationStyle": "custom"
}
},
{
"path": "pages/login/login",
"style": {
"navigationStyle": "custom"
}
}
},
{
"path": "pages/login/login",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/lease/lease",
@ -80,22 +80,33 @@
"style": {
"navigationStyle": "custom"
}
},
{
"path" : "pages/announcementList/announcementList",
"style" :
{
"navigationBarTitleText": "公告列表"
}
},
{
"path" : "pages/announcementList/announcementDetail",
"style" :
{
"navigationBarTitleText" : "公告详细"
}
}
],
},
{
"path": "pages/announcementList/announcementList",
"style": {
"navigationBarTitleText": "公告列表"
}
},
{
"path": "pages/announcementList/announcementDetail",
"style": {
"navigationBarTitleText": "公告详细"
}
},
{
"path": "pages/image-upload/image-upload",
"style": {
"navigationBarTitleText": "图片上传"
}
},
{
"path": "pages/image-upload-demo/image-upload-demo",
"style": {
"navigationBarTitleText": "图片上传演示",
"navigationStyle": "custom"
}
}
],
"tabBar": {
"color": "#999999",
"selectedColor": "#ff6b6b",
@ -121,11 +132,11 @@
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "设备租赁",
"navigationBarBackgroundColor": "#fff"
},
},
"uniIdRouter": {},
"mp-weixin": {
"requiredPrivateInfos": [

View File

@ -0,0 +1,174 @@
<template>
<view class="demo-page">
<u-navbar
:is-back="true"
title="图片上传演示"
title-color="#000"
:border-bottom="false"
:background="true"
></u-navbar>
<view class="content">
<view class="demo-section">
<text class="section-title">图片上传功能演示</text>
<text class="section-desc">点击下方按钮跳转到图片上传页面</text>
</view>
<view class="button-section">
<button class="demo-btn" @click="goToUpload">开始上传图片</button>
</view>
<view v-if="uploadedImageUrl" class="result-section">
<text class="result-title">上传结果</text>
<image
:src="uploadedImageUrl"
mode="widthFix"
class="result-image"
></image>
<text class="result-url">{{ uploadedImageUrl }}</text>
<button class="copy-btn" @click="copyImageUrl">复制图片URL</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
uploadedImageUrl: '', // URL
}
},
onLoad(options) {
//
if (options.imageUrl) {
this.uploadedImageUrl = decodeURIComponent(options.imageUrl)
}
},
methods: {
//
goToUpload() {
uni.navigateTo({
url: '/pages/image-upload/image-upload'
})
},
// URL
copyImageUrl() {
if (this.uploadedImageUrl) {
uni.setClipboardData({
data: this.uploadedImageUrl,
success: () => {
uni.showToast({
title: '图片URL已复制',
icon: 'success'
})
}
})
}
}
}
}
</script>
<style lang="scss" scoped>
.demo-page {
min-height: 100vh;
background: #f5f5f5;
.content {
padding: 30rpx;
}
.demo-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
text-align: center;
.section-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.section-desc {
display: block;
font-size: 26rpx;
color: #666;
line-height: 1.5;
}
}
.button-section {
margin-bottom: 30rpx;
.demo-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #007aff;
color: #fff;
border-radius: 12rpx;
font-size: 32rpx;
border: none;
&:active {
background: #0056cc;
}
}
}
.result-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
.result-title {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.result-image {
width: 100%;
border-radius: 8rpx;
margin-bottom: 20rpx;
}
.result-url {
display: block;
font-size: 24rpx;
color: #007aff;
word-break: break-all;
line-height: 1.4;
margin-bottom: 20rpx;
background: #f8f8f8;
padding: 20rpx;
border-radius: 8rpx;
}
.copy-btn {
width: 100%;
height: 70rpx;
line-height: 70rpx;
background: #f0f0f0;
color: #333;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
&:active {
background: #e0e0e0;
}
}
}
}
</style>

View File

@ -0,0 +1,339 @@
<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>
</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',
})
},
})
},
//
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}`
uni.showToast({
title: '上传成功',
icon: 'success',
})
// URL
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage) {
// 线
// URL
uni.navigateBack({
success: () => {
//
setTimeout(() => {
uni.$emit('image-upload-success', this.uploadResult)
}, 100)
},
})
} else {
// URL
uni.navigateBack({
delta: 1,
success: () => {
// URL
const currentPage = getCurrentPages()[getCurrentPages().length - 1]
if (currentPage && currentPage.onLoad) {
currentPage.onLoad({
imageUrl: encodeURIComponent(this.uploadResult),
})
}
},
})
}
} 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',
})
},
})
}
},
},
}
</script>
<style lang="scss" scoped>
.image-upload-page {
min-height: 100vh;
background: #f5f5f5;
.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: #007aff;
color: #fff;
border-radius: 12rpx;
font-size: 32rpx;
border: none;
margin-bottom: 30rpx;
&:active {
background: #0056cc;
}
&: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>