uniapp APP端 ios保存图片报错解决方案

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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值