index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. import Dialog from 'tdesign-miniprogram/dialog/index';
  2. import Toast from 'tdesign-miniprogram/toast/index';
  3. import { priceFormat } from '../../../utils/util';
  4. import { OrderStatus, ServiceType, ServiceReceiptStatus } from '../config';
  5. import reasonSheet from '../components/reason-sheet/reasonSheet';
  6. import {
  7. fetchRightsPreview,
  8. dispatchConfirmReceived,
  9. fetchApplyReasonList,
  10. dispatchApplyService,
  11. } from '../../../services/order/applyService';
  12. Page({
  13. query: {},
  14. data: {
  15. uploading: false, // 凭证上传状态
  16. canApplyReturn: true, // 是否可退货
  17. goodsInfo: {},
  18. receiptStatusList: [
  19. { desc: '未收到货', status: ServiceReceiptStatus.NOT_RECEIPTED },
  20. { desc: '已收到货', status: ServiceReceiptStatus.RECEIPTED },
  21. ],
  22. applyReasons: [],
  23. serviceType: null, // 20-仅退款,10-退货退款
  24. serviceFrom: {
  25. returnNum: 1,
  26. receiptStatus: { desc: '请选择', status: null },
  27. applyReason: { desc: '请选择', type: null },
  28. // max-填写上限(单位分),current-当前值(单位分),temp输入框中的值(单位元)
  29. amount: { max: 0, current: 0, temp: 0, focus: false },
  30. remark: '',
  31. rightsImageUrls: [],
  32. },
  33. maxApplyNum: 2, // 最大可申请售后的商品数
  34. amountTip: '',
  35. showReceiptStatusDialog: false,
  36. validateRes: {
  37. valid: false,
  38. msg: '',
  39. },
  40. submitting: false,
  41. inputDialogVisible: false,
  42. uploadGridConfig: {
  43. column: 3,
  44. width: 212,
  45. height: 212,
  46. },
  47. serviceRequireType: '',
  48. },
  49. setWatcher(key, callback) {
  50. let lastData = this.data;
  51. const keys = key.split('.');
  52. keys.slice(0, -1).forEach((k) => {
  53. lastData = lastData[k];
  54. });
  55. const lastKey = keys[keys.length - 1];
  56. this.observe(lastData, lastKey, callback);
  57. },
  58. observe(data, k, callback) {
  59. let val = data[k];
  60. Object.defineProperty(data, k, {
  61. configurable: true,
  62. enumerable: true,
  63. set: (value) => {
  64. val = value;
  65. callback();
  66. },
  67. get: () => {
  68. return val;
  69. },
  70. });
  71. },
  72. validate() {
  73. let valid = true;
  74. let msg = '';
  75. // 检查必填项
  76. if (!this.data.serviceFrom.applyReason.type) {
  77. valid = false;
  78. msg = '请填写退款原因';
  79. } else if (!this.data.serviceFrom.amount.current) {
  80. valid = false;
  81. msg = '请填写退款金额';
  82. }
  83. if (this.data.serviceFrom.amount.current <= 0) {
  84. valid = false;
  85. msg = '退款金额必须大于0';
  86. }
  87. this.setData({ validateRes: { valid, msg } });
  88. },
  89. onLoad(query) {
  90. this.query = query;
  91. if (!this.checkQuery()) return;
  92. this.setData({
  93. canApplyReturn: query.canApplyReturn === 'true',
  94. });
  95. this.init();
  96. this.inputDialog = this.selectComponent('#input-dialog');
  97. this.setWatcher('serviceFrom.returnNum', this.validate.bind(this));
  98. this.setWatcher('serviceFrom.applyReason', this.validate.bind(this));
  99. this.setWatcher('serviceFrom.amount', this.validate.bind(this));
  100. this.setWatcher('serviceFrom.rightsImageUrls', this.validate.bind(this));
  101. },
  102. async init() {
  103. try {
  104. await this.refresh();
  105. } catch (e) {}
  106. },
  107. checkQuery() {
  108. const { orderNo, skuId } = this.query;
  109. if (!orderNo) {
  110. Dialog.alert({
  111. content: '请先选择订单',
  112. }).then(() => {
  113. wx.redirectTo({ url: 'pages/order/order-list/index' });
  114. });
  115. return false;
  116. }
  117. if (!skuId) {
  118. Dialog.alert({
  119. content: '请先选择商品',
  120. }).then(() => {
  121. wx.redirectTo(`pages/order/order-detail/index?orderNo=${orderNo}`);
  122. });
  123. return false;
  124. }
  125. return true;
  126. },
  127. async refresh() {
  128. wx.showLoading({ title: 'loading' });
  129. try {
  130. const res = await this.getRightsPreview();
  131. wx.hideLoading();
  132. const goodsInfo = {
  133. id: res.data.skuId,
  134. thumb: res.data.goodsInfo && res.data.goodsInfo.skuImage,
  135. title: res.data.goodsInfo && res.data.goodsInfo.goodsName,
  136. spuId: res.data.spuId,
  137. skuId: res.data.skuId,
  138. specs: ((res.data.goodsInfo && res.data.goodsInfo.specInfo) || []).map((s) => s.specValue),
  139. paidAmountEach: res.data.paidAmountEach,
  140. boughtQuantity: res.data.boughtQuantity,
  141. };
  142. this.setData({
  143. goodsInfo,
  144. 'serviceFrom.amount': {
  145. max: res.data.refundableAmount,
  146. current: res.data.refundableAmount,
  147. },
  148. 'serviceFrom.returnNum': res.data.numOfSku,
  149. amountTip: `最多可申请退款¥ ${priceFormat(res.data.refundableAmount, 2)},含发货运费¥ ${priceFormat(
  150. res.data.shippingFeeIncluded,
  151. 2,
  152. )}`,
  153. maxApplyNum: res.data.numOfSkuAvailable,
  154. });
  155. } catch (err) {
  156. wx.hideLoading();
  157. throw err;
  158. }
  159. },
  160. async getRightsPreview() {
  161. const { orderNo, skuId, spuId } = this.query;
  162. const params = {
  163. orderNo,
  164. skuId,
  165. spuId,
  166. numOfSku: this.data.serviceFrom.returnNum,
  167. };
  168. const res = await fetchRightsPreview(params);
  169. return res;
  170. },
  171. onApplyOnlyRefund() {
  172. wx.setNavigationBarTitle({ title: '申请退款' });
  173. this.setData({ serviceRequireType: 'REFUND_MONEY' });
  174. this.switchReceiptStatus(0);
  175. },
  176. onApplyReturnGoods() {
  177. wx.setNavigationBarTitle({ title: '申请退货退款' });
  178. this.setData({ serviceRequireType: 'REFUND_GOODS' });
  179. const orderStatus = parseInt(this.query.orderStatus);
  180. Promise.resolve()
  181. .then(() => {
  182. if (orderStatus === OrderStatus.PENDING_RECEIPT) {
  183. return Dialog.confirm({
  184. title: '订单商品是否已经收到货',
  185. content: '',
  186. confirmBtn: '确认收货,并申请退货',
  187. cancelBtn: '未收到货',
  188. }).then(() => {
  189. return dispatchConfirmReceived({
  190. parameter: {
  191. logisticsNo: this.query.logisticsNo,
  192. orderNo: this.query.orderNo,
  193. },
  194. });
  195. });
  196. }
  197. return;
  198. })
  199. .then(() => {
  200. this.setData({ serviceType: ServiceType.RETURN_GOODS });
  201. this.switchReceiptStatus(1);
  202. });
  203. },
  204. onApplyReturnGoodsStatus() {
  205. reasonSheet({
  206. show: true,
  207. title: '选择退款原因',
  208. options: this.data.applyReasons.map((r) => ({
  209. title: r.desc,
  210. })),
  211. showConfirmButton: true,
  212. showCancelButton: true,
  213. emptyTip: '请选择退款原因',
  214. }).then((indexes) => {
  215. this.setData({
  216. 'serviceFrom.applyReason': this.data.applyReasons[indexes[0]],
  217. });
  218. });
  219. },
  220. onChangeReturnNum(e) {
  221. const { value } = e.detail;
  222. this.setData({
  223. 'serviceFrom.returnNum': value,
  224. });
  225. },
  226. onApplyGoodsStatus() {
  227. reasonSheet({
  228. show: true,
  229. title: '请选择收货状态',
  230. options: this.data.receiptStatusList.map((r) => ({
  231. title: r.desc,
  232. })),
  233. showConfirmButton: true,
  234. emptyTip: '请选择收货状态',
  235. }).then((indexes) => {
  236. this.setData({
  237. 'serviceFrom.receiptStatus': this.data.receiptStatusList[indexes[0]],
  238. });
  239. });
  240. },
  241. switchReceiptStatus(index) {
  242. const statusItem = this.data.receiptStatusList[index];
  243. // 没有找到对应的状态,则清空/初始化
  244. if (!statusItem) {
  245. this.setData({
  246. showReceiptStatusDialog: false,
  247. 'serviceFrom.receiptStatus': { desc: '请选择', status: null },
  248. 'serviceFrom.applyReason': { desc: '请选择', type: null }, // 收货状态改变时,初始化申请原因
  249. applyReasons: [],
  250. });
  251. return;
  252. }
  253. // 仅选中项与当前项不一致时,才切换申请原因列表applyReasons
  254. if (!statusItem || statusItem.status === this.data.serviceFrom.receiptStatus.status) {
  255. this.setData({ showReceiptStatusDialog: false });
  256. return;
  257. }
  258. this.getApplyReasons(statusItem.status).then((reasons) => {
  259. this.setData({
  260. showReceiptStatusDialog: false,
  261. 'serviceFrom.receiptStatus': statusItem,
  262. 'serviceFrom.applyReason': { desc: '请选择', type: null }, // 收货状态改变时,重置申请原因
  263. applyReasons: reasons,
  264. });
  265. });
  266. },
  267. getApplyReasons(receiptStatus) {
  268. const params = { rightsReasonType: receiptStatus };
  269. return fetchApplyReasonList(params)
  270. .then((res) => {
  271. return res.data.rightsReasonList.map((reason) => ({
  272. type: reason.id,
  273. desc: reason.desc,
  274. }));
  275. })
  276. .catch(() => {
  277. return [];
  278. });
  279. },
  280. onReceiptStatusDialogConfirm(e) {
  281. const { index } = e.currentTarget.dataset;
  282. this.switchReceiptStatus(index);
  283. },
  284. onAmountTap() {
  285. this.setData({
  286. 'serviceFrom.amount.temp': priceFormat(this.data.serviceFrom.amount.current),
  287. 'serviceFrom.amount.focus': true,
  288. inputDialogVisible: true,
  289. });
  290. this.inputDialog.setData({
  291. cancelBtn: '取消',
  292. confirmBtn: '确定',
  293. });
  294. this.inputDialog._onConfirm = () => {
  295. this.setData({
  296. 'serviceFrom.amount.current': this.data.serviceFrom.amount.temp * 100,
  297. });
  298. };
  299. this.inputDialog._onCancel = () => {};
  300. },
  301. // 对输入的值进行过滤
  302. onAmountInput(e) {
  303. let { value } = e.detail;
  304. const regRes = value.match(/\d+(\.?\d*)?/); // 输入中,允许末尾为小数点
  305. value = regRes ? regRes[0] : '';
  306. this.setData({ 'serviceFrom.amount.temp': value });
  307. },
  308. // 失去焦点时,更严格的过滤并转化为float
  309. onAmountBlur(e) {
  310. let { value } = e.detail;
  311. const regRes = value.match(/\d+(\.?\d+)?/); // 失去焦点时,不允许末尾为小数点
  312. value = regRes ? regRes[0] : '0';
  313. value = parseFloat(value) * 100;
  314. if (value > this.data.serviceFrom.amount.max) {
  315. value = this.data.serviceFrom.amount.max;
  316. }
  317. this.setData({
  318. 'serviceFrom.amount.temp': priceFormat(value),
  319. 'serviceFrom.amount.focus': false,
  320. });
  321. },
  322. onAmountFocus() {
  323. this.setData({ 'serviceFrom.amount.focus': true });
  324. },
  325. onRemarkChange(e) {
  326. const { value } = e.detail;
  327. this.setData({
  328. 'serviceFrom.remark': value,
  329. });
  330. },
  331. // 发起申请售后请求
  332. onSubmit() {
  333. this.submitCheck().then(() => {
  334. const params = {
  335. rights: {
  336. orderNo: this.query.orderNo,
  337. refundRequestAmount: this.data.serviceFrom.amount.current,
  338. rightsImageUrls: this.data.serviceFrom.rightsImageUrls,
  339. rightsReasonDesc: this.data.serviceFrom.applyReason.desc,
  340. rightsReasonType: this.data.serviceFrom.receiptStatus.status,
  341. rightsType: this.data.serviceType,
  342. },
  343. rightsItem: [
  344. {
  345. itemTotalAmount: this.data.goodsInfo.price * this.data.serviceFrom.returnNum,
  346. rightsQuantity: this.data.serviceFrom.returnNum,
  347. skuId: this.query.skuId,
  348. spuId: this.query.spuId,
  349. },
  350. ],
  351. refundMemo: this.data.serviceFrom.remark.current,
  352. };
  353. this.setData({ submitting: true });
  354. // 发起申请售后请求
  355. dispatchApplyService(params)
  356. .then((res) => {
  357. Toast({
  358. context: this,
  359. selector: '#t-toast',
  360. message: '申请成功',
  361. icon: '',
  362. });
  363. wx.redirectTo({
  364. url: `/pages/order/after-service-detail/index?rightsNo=${res.data.rightsNo}`,
  365. });
  366. })
  367. .then(() => this.setData({ submitting: false }))
  368. .catch(() => this.setData({ submitting: false }));
  369. });
  370. },
  371. submitCheck() {
  372. return new Promise((resolve) => {
  373. const { msg, valid } = this.data.validateRes;
  374. if (!valid) {
  375. Toast({
  376. context: this,
  377. selector: '#t-toast',
  378. message: msg,
  379. icon: '',
  380. });
  381. return;
  382. }
  383. resolve();
  384. });
  385. },
  386. handleSuccess(e) {
  387. const { files } = e.detail;
  388. this.setData({
  389. 'sessionFrom.rightsImageUrls': files,
  390. });
  391. },
  392. handleRemove(e) {
  393. const { index } = e.detail;
  394. const {
  395. sessionFrom: { rightsImageUrls },
  396. } = this.data;
  397. rightsImageUrls.splice(index, 1);
  398. this.setData({
  399. 'sessionFrom.rightsImageUrls': rightsImageUrls,
  400. });
  401. },
  402. handleComplete() {
  403. this.setData({
  404. uploading: false,
  405. });
  406. },
  407. handleSelectChange() {
  408. this.setData({
  409. uploading: true,
  410. });
  411. },
  412. });