From b87ce7d43e5b06f66a3a7eeaeff90ea8007e0e11 Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Fri, 21 Nov 2025 16:02:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=B8=E5=85=91=E6=8D=A2=E7=A0=81=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=B8=8B=E8=BD=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- http/api/market/couponRedemption.js | 2 +- http/http.js | 407 +++++++++++++---------- pageMarket/couponExchangeCode/detail.vue | 66 +++- pages.json | 2 +- utils/downloadFile.js | 199 +++++++++++ 5 files changed, 482 insertions(+), 194 deletions(-) create mode 100644 utils/downloadFile.js diff --git a/http/api/market/couponRedemption.js b/http/api/market/couponRedemption.js index 74b5956..25b8580 100644 --- a/http/api/market/couponRedemption.js +++ b/http/api/market/couponRedemption.js @@ -67,7 +67,7 @@ export function codeList(params) { }); } export function codeExport(params) { - return request({ + return http.download({ url: urlType + `/admin/couponRedemption/code/export`, method: "GET", params: { diff --git a/http/http.js b/http/http.js index 2837d66..3059424 100644 --- a/http/http.js +++ b/http/http.js @@ -1,26 +1,26 @@ /** * HTTP的封装, 基于uni.request * 包括: 通用响应结果的处理 和 业务的增删改查函数 - * + * * @author terrfly * @site https://www.jeequan.com * @date 2021/12/16 18:35 */ // 设置env配置文件 -import envConfig from '@/env/config.js' +import envConfig from "@/env/config.js"; // 导入全局属性 -import appConfig from '@/config/appConfig.js' -import storageManage from '@/commons/utils/storageManage.js' -import infoBox from "@/commons/utils/infoBox.js" -import go from '@/commons/utils/go.js'; -import { reject } from 'lodash'; +import appConfig from "@/config/appConfig.js"; +import storageManage from "@/commons/utils/storageManage.js"; +import infoBox from "@/commons/utils/infoBox.js"; +import go from "@/commons/utils/go.js"; +import { reject } from "lodash"; // 设置node环境 // envConfig.changeEnv(storageManage.env('production')) -envConfig.changeEnv(storageManage.env('development')) +envConfig.changeEnv(storageManage.env("development")); // 测试服 // #ifdef H5 -let baseUrl = '/api/' +let baseUrl = "/api/"; // #endif // #ifndef H5 // let baseUrl = 'https://tapi.cashier.sxczgkj.cn/' @@ -29,207 +29,248 @@ let baseUrl = '/api/' //正式 // let baseUrl = 'https://cashier.sxczgkj.com/' -let baseUrl = appConfig.env.JEEPAY_BASE_URL +let baseUrl = appConfig.env.JEEPAY_BASE_URL; // #endif -const loadingShowTime = 200 +const loadingShowTime = 200; -function getHeader(){ - const headerObject={} - headerObject["token"] = storageManage.token() - headerObject["shopId"] = uni.getStorageSync("shopInfo").id - headerObject["platformType"] = 'APP' - - return headerObject +function getHeader() { + const headerObject = {}; + headerObject["token"] = storageManage.token(); + headerObject["shopId"] = uni.getStorageSync("shopInfo").id; + headerObject["platformType"] = "APP"; + + return headerObject; } -// 通用处理逻辑 +// 通用处理逻辑 function commonsProcess(showLoading, httpReqCallback) { + // 判断是否请求完成(用作 是否loading ) + // 包括: 'ing', 'ingLoading', 'finish' + let reqState = "ing"; - // 判断是否请求完成(用作 是否loading ) - // 包括: 'ing', 'ingLoading', 'finish' - let reqState = 'ing' + // 是否已经提示的错误信息 + let isShowErrorToast = false; - // 是否已经提示的错误信息 - let isShowErrorToast = false + // 请求完成, 需要处理的动作 + let reqFinishFunc = () => { + if (reqState == "ingLoading") { + // 关闭loading弹层 + infoBox.hideLoading(); + } + reqState = "finish"; // 请求完毕 + }; + // 明确显示loading + if (showLoading) { + // xx ms内响应完成,不提示loading + setTimeout(() => { + if (reqState == "ing") { + reqState = "ingLoading"; + infoBox.showLoading(); + } + }, loadingShowTime); + } - // 请求完成, 需要处理的动作 - let reqFinishFunc = () => { + return httpReqCallback() + .then((httpData) => { + reqFinishFunc(); // 请求完毕的动作 + // 从http响应数据中解构响应数据 [ 响应码、 bodyData ] + let { statusCode, data } = httpData; + // 避免混淆重新命名 + let bodyData = data; + if (statusCode == 500) { + isShowErrorToast = true; + return Promise.reject(bodyData); // 跳转到catch函数 + } + if (statusCode == 501) { + // storageManage.token(null, true) + // 提示信息 + isShowErrorToast = true; + // infoBox.showErrorToast('请登录').then(() => { + // go.to("PAGES_LOGIN", {}, go.GO_TYPE_RELAUNCH) + // }) + return Promise.reject(bodyData); // 跳转到catch函数 + } + // http响应码不正确 + if (statusCode != 200 && statusCode != 204 && statusCode != 201) { + isShowErrorToast = true; + bodyData.msg = + bodyData.msg == "Bad credentials" ? "用户名或密码错误" : bodyData.msg; + infoBox.showToast(bodyData.msg || "服务器异常"); + return Promise.reject(bodyData); // 跳转到catch函数 + } - if (reqState == 'ingLoading') { // 关闭loading弹层 - infoBox.hideLoading() - } - reqState = 'finish' // 请求完毕 - } + // // 业务响应异常 + if (bodyData.hasOwnProperty("code") && bodyData.code != 200) { + isShowErrorToast = true; + infoBox.showToast(bodyData.msg); + // if (bodyData.code == 5005) { // 密码已过期, 直接跳转到更改密码页面 + // uni.reLaunch({ + // url: '/pageUser/setting/updatePwd' + // }) + // } + // if(bodyData.code == 500){ // 密码已过期, 直接跳转到更改密码页面 + // uni.redirectTo({url: '/pages/login/index'}) + // } + return Promise.reject(bodyData); // 跳转到catch函数 + } - // 明确显示loading - if (showLoading) { - // xx ms内响应完成,不提示loading - setTimeout(() => { - if (reqState == 'ing') { - reqState = 'ingLoading' - infoBox.showLoading() - } - }, loadingShowTime) - } + // 构造请求成功的响应数据 + return Promise.resolve(bodyData.data); + }) + .catch((res) => { + console.log(res); + if (res.code == 501) { + storageManage.token(null, true); + infoBox.showToast("登录过期,请重新登录").then(() => { + uni.redirectTo({ url: "/pages/login/index" }); + reject(); + }); + } + // if(res.status==400){ + // storageManage.token(null, true) + // infoBox.showErrorToast('').then(() => { + // go.to("PAGES_LOGIN", {}, go.GO_TYPE_RELAUNCH) + // }) + // } + if (res.code == 500) { + infoBox.showToast(res.msg || "服务器异常").then(() => {}); + } + // if(res&&res.msg){ + // infoBox.showErrorToast(res.msg) + // } + reqFinishFunc(); // 请求完毕的动作 - return httpReqCallback().then((httpData) => { - reqFinishFunc(); // 请求完毕的动作 - // 从http响应数据中解构响应数据 [ 响应码、 bodyData ] - let { - statusCode, - data - } = httpData - // 避免混淆重新命名 - let bodyData = data - if (statusCode == 500) { - isShowErrorToast = true - return Promise.reject(bodyData) // 跳转到catch函数 - } - if (statusCode == 501) { - // storageManage.token(null, true) - // 提示信息 - isShowErrorToast = true - // infoBox.showErrorToast('请登录').then(() => { - // go.to("PAGES_LOGIN", {}, go.GO_TYPE_RELAUNCH) - // }) - return Promise.reject(bodyData) // 跳转到catch函数 - } - // http响应码不正确 - if (statusCode != 200 && statusCode != 204 && statusCode != 201) { - isShowErrorToast = true - bodyData.msg=bodyData.msg=='Bad credentials'?'用户名或密码错误':bodyData.msg - infoBox.showToast(bodyData.msg || '服务器异常') - return Promise.reject(bodyData) // 跳转到catch函数 - } - - // // 业务响应异常 - if (bodyData.hasOwnProperty('code') && bodyData.code != 200) { - isShowErrorToast = true - infoBox.showToast(bodyData.msg) - // if (bodyData.code == 5005) { // 密码已过期, 直接跳转到更改密码页面 - // uni.reLaunch({ - // url: '/pageUser/setting/updatePwd' - // }) - // } - // if(bodyData.code == 500){ // 密码已过期, 直接跳转到更改密码页面 - // uni.redirectTo({url: '/pages/login/index'}) - // } - return Promise.reject(bodyData) // 跳转到catch函数 - } - - // 构造请求成功的响应数据 - return Promise.resolve(bodyData.data) - - }).catch(res => { - console.log(res) - if(res.code==501){ - storageManage.token(null, true) - infoBox.showToast('登录过期,请重新登录').then(() => { - uni.redirectTo({url: '/pages/login/index'}) - reject() - }) - } - // if(res.status==400){ - // storageManage.token(null, true) - // infoBox.showErrorToast('').then(() => { - // go.to("PAGES_LOGIN", {}, go.GO_TYPE_RELAUNCH) - // }) - // } - if(res.code==500){ - infoBox.showToast(res.msg||'服务器异常').then(() => {}) - } - // if(res&&res.msg){ - // infoBox.showErrorToast(res.msg) - // } - reqFinishFunc(); // 请求完毕的动作 - - // 如果没有提示错误, 那么此处提示 异常。 - if (!isShowErrorToast) { - infoBox.showToast(`请求网络异常`) - } - - return Promise.reject(res) - - }).finally(() => { // finally 是 then结束后再执行, 此处不适用。 需要在请求完成后立马调用: reqFinishFunc() - - }); + // 如果没有提示错误, 那么此处提示 异常。 + if (!isShowErrorToast) { + infoBox.showToast(`请求网络异常`); + } + return Promise.reject(res); + }) + .finally(() => { + // finally 是 then结束后再执行, 此处不适用。 需要在请求完成后立马调用: reqFinishFunc() + }); } - // 默认 显示loading(控制 xxs 内 不提示loading ) function req(uri, data, method = "GET", showLoading = true, extParams = {}) { - // headerObject[appConfig.tokenKey] = storageManage.token() - return commonsProcess(showLoading, () => { - return uni.request( - Object.assign({ - url: baseUrl + uri, - data: data, - method: method, - header: getHeader() - }, extParams) - ) - }) + // headerObject[appConfig.tokenKey] = storageManage.token() + return commonsProcess(showLoading, () => { + return uni.request( + Object.assign( + { + url: baseUrl + uri, + data: data, + method: method, + header: getHeader(), + }, + extParams + ) + ); + }); } - // 默认 显示loading(控制 xxs 内 不提示loading ) function request(args) { - const { - url, - data, - params, - method = "GET", - showLoading = true, - extParams = {} - } = args - let headerObject = {} - // headerObject[appConfig.tokenKey] = storageManage.token() - return commonsProcess(showLoading, () => { - return uni.request( - Object.assign({ - url: baseUrl + url, - data: params||data, - method: method, - header: getHeader() - }, extParams) - - ) - }) + const { + url, + data, + params, + method = "GET", + showLoading = true, + extParams = {}, + } = args; + let headerObject = {}; + // headerObject[appConfig.tokenKey] = storageManage.token() + return commonsProcess(showLoading, () => { + return uni.request( + Object.assign( + { + url: baseUrl + url, + data: params || data, + method: method, + header: getHeader(), + }, + extParams + ) + ); + }); } - - // 上传 function upload(uri, data, file, showLoading = true, extParams = {}) { - // 放置token - let headerObject = {} - // headerObject[appConfig.tokenKey] = storageManage.token() + // 放置token + let headerObject = {}; + // headerObject[appConfig.tokenKey] = storageManage.token() - return commonsProcess(showLoading, () => { - return uni.uploadFile( - Object.assign({ - url: baseUrl + uri, - formData: data, - name: "file", - filePath: file.path||file.url, - header: getHeader() - }, extParams) - ).then((httpData) => { - // uni.upload 返回bodyData 的是 string类型。 需要解析。 - httpData.data = JSON.parse(httpData.data) - return Promise.resolve(httpData) - }).catch(err=>{ - uni.hideLoading() - infoBox.showErrorToast(`上传失败`) - }) - }) + return commonsProcess(showLoading, () => { + return uni + .uploadFile( + Object.assign( + { + url: baseUrl + uri, + formData: data, + name: "file", + filePath: file.path || file.url, + header: getHeader(), + }, + extParams + ) + ) + .then((httpData) => { + // uni.upload 返回bodyData 的是 string类型。 需要解析。 + httpData.data = JSON.parse(httpData.data); + return Promise.resolve(httpData); + }) + .catch((err) => { + uni.hideLoading(); + infoBox.showErrorToast(`上传失败`); + }); + }); +} + +function returnParams(params) { + let returnStr = ""; + if (params) { + returnStr = "?"; + for (let key in params) { + returnStr += `${key}=${params[key]}&`; + } + returnStr = returnStr.substring(0, returnStr.length - 1); + } + return returnStr; +} +// 下载文件 +function download(args) { + const { + url, + data, + params, + method = "GET", + showLoading = true, + extParams = {}, + } = args; + let headerObject = {}; + return uni + .downloadFile({ + url: baseUrl + url + returnParams(params), + + header: getHeader(), + }) + .then((httpData) => { + return Promise.resolve(httpData); + }) + .catch((err) => { + uni.hideLoading(); + infoBox.showErrorToast(`下载失败`); + }); } export default { - req: req, - request, - upload: upload -} \ No newline at end of file + req: req, + request, + upload: upload, + download, +}; diff --git a/pageMarket/couponExchangeCode/detail.vue b/pageMarket/couponExchangeCode/detail.vue index df65294..2c0a752 100644 --- a/pageMarket/couponExchangeCode/detail.vue +++ b/pageMarket/couponExchangeCode/detail.vue @@ -31,9 +31,9 @@ - 优惠券 - - {{ returnCoupon }} + 优惠券 + + {{ returnCoupon }} 修改 @@ -58,10 +58,11 @@ placeholder="输入兑换码" class="u-font-28 u-m-l-20" placeholder-class="color-999 u-font-28" - v-model="query.name" + @input="codeChange" + v-model="query.code" /> - 导出 + 导出 @@ -130,7 +131,7 @@ import CouponList from "@/pageMarket/components/coupon-list.vue"; import Modal from "@/pageMarket/components/modal.vue"; import * as couponRedemptionApi from "@/http/api/market/couponRedemption.js"; - +import { saveFileFromTemp } from "@/utils/downloadFile.js"; import { reactive, toRefs, @@ -188,6 +189,8 @@ const form = reactive({ onLoad((opt) => { const item = uni.getStorageSync("couponRedemptionItem"); Object.assign(form, item); + console.log(form); + getList(); }); const query = reactive({ status: "", @@ -233,10 +236,55 @@ function refresh() { query.page = 1; getList(); } -watch(() => query, refresh); -onMounted(() => { - getList(); +watch(() => query.status, refresh); + +//节流 +const throttle = (fn, delay = 500) => { + let timer = null; + return function () { + if (timer) { + return; + } + timer = setTimeout(() => { + fn.apply(this, arguments); + timer = null; + }, delay); + }; +}; + +const codeChange = throttle(() => { + console.log(query.code); + refresh(); }); + +function exportCode() { + couponRedemptionApi + .codeExport({ + status: query.status, + redemptionId: form.id, + code: query.code, + }) + .then((res) => { + console.log(res); + if (res.statusCode == 200) { + saveFileFromTemp({ + tempFilePath: res.tempFilePath, + fileName: '优惠券兑换码.xlsx', + isNowOpen:true + }); + // uni.getFileSystemManager().saveFile({ + // tempFilePath: res.tempFilePath, + // success: function (res) { + // console.log(res); + // }, + // fail: function (err) { + // console.log(err); + // } + // }); + } + }); +} + onReachBottom(() => { console.log("触底"); if (isEnd.value) { diff --git a/pages.json b/pages.json index e363174..fa709e3 100644 --- a/pages.json +++ b/pages.json @@ -740,7 +740,7 @@ "style": { "navigationBarTitleText": "券兑换码详情" } - }, + } ] diff --git a/utils/downloadFile.js b/utils/downloadFile.js new file mode 100644 index 0000000..4e01b5a --- /dev/null +++ b/utils/downloadFile.js @@ -0,0 +1,199 @@ +/** + * 多端兼容文件保存(基于已下载的临时路径,无需重复download) + * 核心:传入的url是uni.downloadFile返回的tempFilePath,直接用fs.saveFile保存 + * @param {string} tempFilePath - 已下载的文件临时路径(必填,来自downloadFile返回值) + * @param {string} [fileName] - 自定义保存文件名(可选,默认从临时路径提取) + * @returns {Promise} - 保存结果Promise + */ +export function saveFileFromTemp(args) { + const {tempFilePath, fileName,isNowOpen=false}=args + // 前置校验:临时路径必填 + if (!tempFilePath) { + uni.showToast({ title: "文件临时路径不能为空", icon: "none" }); + return Promise.reject(new Error("临时路径不能为空")); + } + + // 共用逻辑:从临时路径/URL提取默认文件名(适配各端临时路径格式) + const getDefaultFileName = () => { + let name = `文件_${Date.now()}`; + try { + // 处理微信小程序临时路径(如:wxfile://tmp/xxx.pdf) + if ( + tempFilePath.includes("wxfile://") || + tempFilePath.includes("/tmp/") + ) { + const lastSlashIndex = tempFilePath.lastIndexOf("/"); + if (lastSlashIndex !== -1) { + name = tempFilePath.slice(lastSlashIndex + 1); + } + } + // 处理App临时路径(如:_doc/tmp/xxx.png)或普通URL + else { + const urlObj = new URL(tempFilePath, window.location.origin); // 兼容相对路径 + const pathname = urlObj.pathname; + const lastSlashIndex = pathname.lastIndexOf("/"); + if (lastSlashIndex !== -1) { + name = pathname.slice(lastSlashIndex + 1).split("?")[0]; + } + } + // 补充文件后缀(如果没有) + if (!name.includes(".")) { + name += ".bin"; // 兜底后缀,可根据实际业务调整(如:.pdf/.png) + } + } catch (e) { + name = `文件_${Date.now()}.bin`; + } + return fileName || name; + }; + const targetFileName = getDefaultFileName(); + + // -------------------------- H5端处理(临时路径直接触发下载) -------------------------- + // #ifdef H5 + return new Promise((resolve, reject) => { + // H5的临时路径可能是blob:xxx格式,直接用a标签下载 + const link = document.createElement("a"); + link.href = tempFilePath; + link.download = targetFileName; + link.target = "_blank"; + link.style.display = "none"; + + link.addEventListener("error", () => { + uni.showToast({ + title: "保存失败:H5临时路径无效或跨域", + icon: "none", + duration: 3000, + }); + reject(new Error("H5临时路径保存失败")); + }); + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + uni.showToast({ title: "保存成功,请查看浏览器下载列表", icon: "none" }); + resolve(); + }); + // #endif + + // -------------------------- 微信小程序端处理(fs.saveFile 保存临时路径) -------------------------- + // #ifdef MP-WEIXIN + return new Promise((resolve, reject) => { + const fs = uni.getFileSystemManager(); + // 微信小程序保存路径(沙箱目录) + const saveFilePath = `${wx.env.USER_DATA_PATH}/${targetFileName}`; + + // 直接保存临时路径(无需下载) + fs.saveFile({ + tempFilePath: tempFilePath, // 传入已下载的临时路径 + filePath: saveFilePath, + success: (saveRes) => { + if(isNowOpen){ + uni.openDocument({ + filePath: saveRes.savedFilePath, + showMenu:true, + success: (openRes) => { + uni.showToast({ + title: "文件已打开", + icon: "none", + duration: 1500, + }); + }, + fail: (openErr) => { + uni.showToast({ + title: `打开文件失败:${openErr.errMsg}`, + icon: "none", + duration: 1500, + }); + }, + }); + }else{ + uni.showModal({ + title: "保存成功", + content: `文件已保存至:微信 → 我 → 文件 → 下载管理`, + showCancel: false, + }); + } + + resolve(saveRes); + }, + fail: (saveErr) => { + uni.showToast({ title: `保存失败:${saveErr.errMsg}`, icon: "none" }); + reject(saveErr); + }, + }); + }); + // #endif + + // -------------------------- App端处理(fs.saveFile 保存临时路径) -------------------------- + // #ifdef APP + return new Promise(async (resolve, reject) => { + const fs = uni.getFileSystemManager(); + // App保存路径(沙箱目录) + const saveFilePath = `${uni.env.USER_DATA_PATH}/${targetFileName}`; + + // Android 6.0+ 申请存储权限 + if (uni.getSystemInfoSync().platform === "android") { + try { + const permRes = await uni.requestPermissions({ + scope: "scope.writeExternalStorage", + }); + if (!permRes[0].authSetting["scope.writeExternalStorage"]) { + uni.showModal({ + title: "权限不足", + content: "请授予存储权限后重试(设置 → 应用 → 权限管理)", + showCancel: false, + }); + return reject(new Error("存储权限未授予")); + } + } catch (permErr) { + uni.showToast({ + title: `权限申请失败:${permErr.errMsg}`, + icon: "none", + }); + return reject(permErr); + } + } + + // 直接保存临时路径(无需下载) + fs.saveFile({ + tempFilePath: tempFilePath, // 传入已下载的临时路径 + filePath: saveFilePath, + success: (saveRes) => { + const platform = uni.getSystemInfoSync().platform; + uni.showModal({ + title: "保存成功", + content: + platform === "ios" + ? "文件已保存至应用沙箱,点击「查看」打开" + : `文件路径:内部存储/Android/data/应用包名/files/${targetFileName}`, + confirmText: "查看", + cancelText: "取消", + success: (modalRes) => { + if (modalRes.confirm) { + uni.openDocument({ + filePath: saveRes.savedFilePath, + showMenu: true, + fail: (openErr) => { + uni.showToast({ + title: `无法打开:${openErr.errMsg}`, + icon: "none", + }); + }, + }); + } + }, + }); + resolve(saveRes); + }, + fail: (saveErr) => { + uni.showToast({ title: `保存失败:${saveErr.errMsg}`, icon: "none" }); + reject(saveErr); + }, + }); + }); + // #endif + + // 其他未适配平台 + return Promise.reject(new Error("当前平台暂不支持文件保存")); +} +