1、报错信息
{
"errMsg": "saveImageToPhotosAlbum:fail [Gallery:3302]未能完成操作。(PHPhotosErrorDomain错误3302。),",
"errCode": -100,
"code": -100
}
2、分析
在安卓端测试uni.saveImageToPhotosAlbum保存图片正常,在ios端报错,原因可能是:因为后端对于预览图片的接口直接是通过传入文件id,然后后端查询到路径,然后回写图片数据,没有使用nginx,导致图片的url没有后缀,uniapp通过uni.downloadFile下载到临时路径没有后缀名,安卓正常,ios报错。
3、解决方案
解决方案大致分为两步:
(1)、通过downloadFile的临时路径然后再进行一次另存,增加后缀名
(2)、然后调用saveImageToPhotosAlbum进行保存
关键方法如下:
/**
* 重命名文件,加上指定后缀
* @param {string} oldPath - 原文件路径
* @param {string} fileSuffix - 文件后缀,例如 '.jpg'
* @returns {Promise<string>} - 返回重命名后的文件路径字符串
*/
renameImage(oldPath, fileSuffix) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
oldPath,
(entry) => {
entry.getParent(
(parentEntry) => {
const newName = entry.name + fileSuffix;
entry.moveTo(
parentEntry,
newName,
(newFileEntry) => {
resolve(newFileEntry.fullPath);
},
(err) => {
console.error("文件移动失败", err);
reject(`文件移动失败: ${err.message}`);
}
);
},
(err) => {
console.error("获取父目录失败", err);
reject(`获取父目录失败: ${err.message}`);
}
);
},
(err) => {
console.error("解析文件路径失败", err);
reject(`解析文件路径失败: ${err.message}`);
}
);
});
},
// 异步保存图片方法
async saveImage() {
try {
// 直接下载图片
const downloadResult = await new Promise((resolve, reject) => {
uni.downloadFile({
url: this.imageUrl,
success: resolve,
fail: reject,
});
});
if (downloadResult.statusCode === 200) {
// 下载成功后保存图片到相册
const newPath = await this.renameImage(
downloadResult.tempFilePath,
this.fileSuffix
);
await new Promise((resolve, reject) => {
uni.saveImageToPhotosAlbum({
filePath: newPath,
success: resolve,
fail: reject,
});
});
uni.showToast({
title: "保存成功",
icon: "success",
});
this.closeOptionsPopup();
} else {
uni.showToast({
title: "保存失败",
icon: "none",
});
throw new Error("下载失败");
}
} catch (err) {
console.error("操作失败", err);
uni.showToast({
title: "操作失败",
icon: "none",
});
}
},
4、整体页面代码
<template>
<view class="notify">
<uni-card
v-for="(item, index) in notifyList"
:key="index"
:title="
findDataInfo(
dictData.notification_categories,
item.attachTag,
'value',
'text'
)
"
>
<text class="uni-body">
{{ item.attachName }}
</text>
<!-- <uni-list>
<uni-list-item title="查看附件" showArrow></uni-list-item>
</uni-list> -->
<view slot="actions" class="card-actions no-border">
<view
class="card-actions-item"
@click="viewAttachments(item.attachId, item.attachExt)"
>
<uni-icons type="bars" size="18" color="#999"></uni-icons>
<text class="card-actions-item-text">查看附件</text>
</view>
</view>
</uni-card>
<!-- 图片预览的弹出框 -->
<uni-popup ref="popup" type="center" :is-mask-click="false">
<view class="popup-container">
<view class="image-wrapper">
<image
:src="imageUrl"
mode="aspectFit"
class="preview-image"
@longpress="openOptionsPopup"
></image>
</view>
<uni-icons
type="closeempty"
size="25"
class="close-button"
@click="closeImagePopup"
></uni-icons>
</view>
</uni-popup>
<!-- 保存图片选项的弹出框 -->
<uni-popup ref="optionsPopup" type="bottom" :is-mask-click="true">
<view class="options-container">
<view class="option-item" @click="saveImage">保存图片</view>
<!-- <view class="separator"></view> -->
<view class="gap"></view>
<view class="cancel-item" @click="closeOptionsPopup">取消</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { queryNotifyList } from "@/api/work/notify";
import { getDictsByCodeValue } from "@/api/index";
import { toast } from "@/utils/common";
import config from "@/config";
export default {
components: {},
data() {
return {
//字典值
dicts: [
{
codeName: "notification_categories",
codeId: "NotificationCategories",
},
],
dictMap: new Map(),
//字典数据
dictData: {},
notifyList: [],
imageUrl: "",
fileSuffix: "",
};
},
computed: {},
methods: {
//查询通知列表
getNotifyList() {
const param = {
pageNumber: 1,
pageSize: 10000,
params: {
attachType: "01",
},
};
queryNotifyList(param).then((res) => {
this.notifyList = res.rows;
});
},
//查看附件
viewAttachments(attachId, attachExt) {
if (!attachId) {
toast("暂无附件");
return;
}
const baseUrl = config.baseUrl;
if (attachExt === ".pdf") {
this.$tab.navigateTo(
"/pages/common/pdfview/index?url=" +
baseUrl +
"/rest/platform/attachment/showPdf?picId=" +
attachId
);
} else {
this.imageUrl =
baseUrl + "/rest/platform/attachment/showPic?picId=" + attachId;
this.fileSuffix = attachExt;
this.openImagePopup();
}
},
// 异步保存图片方法
async saveImage() {
try {
// 直接下载图片
const downloadResult = await new Promise((resolve, reject) => {
uni.downloadFile({
url: this.imageUrl,
success: resolve,
fail: reject,
});
});
if (downloadResult.statusCode === 200) {
// 下载成功后保存图片到相册
const newPath = await this.renameImage(
downloadResult.tempFilePath,
this.fileSuffix
);
await new Promise((resolve, reject) => {
uni.saveImageToPhotosAlbum({
filePath: newPath,
success: resolve,
fail: reject,
});
});
uni.showToast({
title: "保存成功",
icon: "success",
});
this.closeOptionsPopup();
} else {
uni.showToast({
title: "保存失败",
icon: "none",
});
throw new Error("下载失败");
}
} catch (err) {
console.error("操作失败", err);
uni.showToast({
title: "操作失败",
icon: "none",
});
}
},
/**
* 重命名文件,加上指定后缀
* @param {string} oldPath - 原文件路径
* @param {string} fileSuffix - 文件后缀,例如 '.jpg'
* @returns {Promise<string>} - 返回重命名后的文件路径字符串
*/
renameImage(oldPath, fileSuffix) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
oldPath,
(entry) => {
entry.getParent(
(parentEntry) => {
const newName = entry.name + fileSuffix;
entry.moveTo(
parentEntry,
newName,
(newFileEntry) => {
resolve(newFileEntry.fullPath);
},
(err) => {
console.error("文件移动失败", err);
reject(`文件移动失败: ${err.message}`);
}
);
},
(err) => {
console.error("获取父目录失败", err);
reject(`获取父目录失败: ${err.message}`);
}
);
},
(err) => {
console.error("解析文件路径失败", err);
reject(`解析文件路径失败: ${err.message}`);
}
);
});
},
// 获取字典数据
async getDictsData() {
for (const dict of this.dicts) {
const response = await getDictsByCodeValue(dict.codeId);
this.dictMap.set(dict.codeName, response);
}
// 将字典名和对应数据的数组转换为对象
this.dictData = Object.fromEntries(Array.from(this.dictMap.entries()));
},
/**
* @dataList 数据集合
* @original 源数据
* @matchKey 要匹配的key
* @originalKey 最终需要获取的key
*/
findDataInfo(dataList, original, matchKey, originalKey) {
if (!dataList) {
return original;
}
const dataItem = dataList.find((item) => item[matchKey] == original);
return dataItem ? dataItem[originalKey] : original; // 如果找不到,则返回原始ID
},
openImagePopup() {
this.$refs.popup.open();
},
closeImagePopup() {
this.$refs.popup.close();
},
openOptionsPopup() {
this.$refs.optionsPopup.open();
},
closeOptionsPopup() {
this.$refs.optionsPopup.close();
},
},
watch: {},
async created() {
await this.getDictsData();
await this.getNotifyList();
},
// 页面周期函数--监听页面加载
onLoad() {},
// 页面周期函数--监听页面初次渲染完成
onReady() {},
// 页面周期函数--监听页面显示(not-nvue)
onShow() {},
// 页面周期函数--监听页面隐藏
onHide() {},
// 页面周期函数--监听页面卸载
onUnload() {},
// 页面处理函数--监听用户下拉动作
// onPullDownRefresh() { uni.stopPullDownRefresh(); },
// 页面处理函数--监听用户上拉触底
// onReachBottom() {},
// 页面处理函数--监听页面滚动(not-nvue)
// onPageScroll(event) {},
// 页面处理函数--用户点击右上角分享
// onShareAppMessage(options) {},
};
</script>
<style lang="scss">
.notify {
padding-bottom: 10px;
}
.card-actions {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
height: 45px;
border-top: 1px #eee solid;
}
.card-actions-item {
display: flex;
flex-direction: row;
align-items: center;
}
.card-actions-item-text {
font-size: 12px;
color: #666;
margin-left: 5px;
}
.no-border {
border-width: 0;
}
/* 弹出框样式 */
.popup-container {
position: relative;
width: 100%;
height: 100%;
background-color: white;
border-radius: 10px;
overflow: hidden;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
/* 图片包装样式 */
.image-wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
/* 预览图片样式 */
.preview-image {
object-fit: contain;
}
/* 关闭按钮样式 */
.close-button {
position: absolute;
top: 10px;
right: 10px;
background-color: transparent;
border: none;
font-size: 24px;
color: black;
}
/* 选项弹出框样式 */
.options-container {
padding: 0;
background-color: #f5f5f5;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15);
overflow: hidden; /* 确保圆角效果 */
}
/* 操作项样式 */
.option-item {
padding: 15px;
font-size: 15px;
text-align: center;
background-color: white;
}
/* 分隔线样式 */
.separator {
height: 1px;
background-color: #e5e5e5;
}
/* 间隔样式 */
.gap {
height: 6px;
background-color: #f5f5f5;
}
/* 取消按钮样式 */
.cancel-item {
padding: 15px;
font-size: 15px;
text-align: center;
background-color: white;
}
</style>