index.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <script setup lang="tsx">
  2. import { reactive, ref, unref, useTemplateRef, watch } from 'vue';
  3. import { NImage, NTag } from 'naive-ui';
  4. import { fetchGetAfterSalesOrderList, fetchGetAfterSalesStatusNum } from '@/service/api/delivery/after-sales-order';
  5. import { useAppStore } from '@/store/modules/app';
  6. import { defaultTransform, useNaivePaginatedTable } from '@/hooks/common/table';
  7. import { copyTextToClipboard } from '@/utils/zt';
  8. import { $t } from '@/locales';
  9. import { useForm } from '@/components/zt/Form/hooks/useForm';
  10. import NormalMoadl from '../normal-order/component/normal-modal.vue';
  11. import { SearchForm, orderStatus } from '../normal-order/normal-order';
  12. import { refundEnum, refundStatus } from './after-sales-order';
  13. const appStore = useAppStore();
  14. const checkedRowKeys = ref([]);
  15. const activeTab = ref(0);
  16. const statusList = ref<{ label: string; value: number; num?: number; key: string }[]>([]);
  17. const orderMoadl = useTemplateRef('orderMoadl');
  18. const [registerSearchForm, { getFieldsValue }] = useForm({
  19. schemas: SearchForm,
  20. showAdvancedButton: false,
  21. labelWidth: 120,
  22. layout: 'horizontal',
  23. gridProps: {
  24. cols: '1 xl:4 s:1 l:3',
  25. itemResponsive: true
  26. },
  27. collapsedRows: 1
  28. });
  29. const searchForm = ref();
  30. const searchParams = reactive({
  31. current: 1,
  32. size: 10
  33. });
  34. const { columns, data, loading, getData, mobilePagination } = useNaivePaginatedTable({
  35. api: () => fetchGetAfterSalesOrderList({ ...searchParams, returnMoneySts: activeTab.value, ...unref(searchForm) }),
  36. transform: response => defaultTransform(response),
  37. paginationProps: {
  38. pageSizes: [10, 20, 50, 100, 150, 200]
  39. },
  40. onPaginationParamsChange: params => {
  41. searchParams.current = Number(params.page);
  42. searchParams.size = Number(params.pageSize);
  43. },
  44. columns: () => [
  45. {
  46. key: 'orderItems',
  47. title: '商品',
  48. align: 'left',
  49. width: 200,
  50. colSpan: (_rowData, _rowIndex) => 2,
  51. render: row => {
  52. return (
  53. <div>
  54. <div class={'mb3 flex items-center'}>
  55. <n-tag>
  56. <div class={'flex items-center'}>
  57. 退款编号:{row.refundSn}
  58. <div onClick={() => handleCopy(row.refundSn)}>
  59. <svgIcon icon={'bxs:copy'} class={'mx-3 cursor-pointer text-[#f97316]'}></svgIcon>
  60. </div>
  61. 订单编号:{row.orderNumber}
  62. <div onClick={() => handleCopy(row.orderNumber)}>
  63. <svgIcon icon={'bxs:copy'} class={'mx-3 cursor-pointer text-[#f97316]'}></svgIcon>
  64. </div>
  65. 申请时间:
  66. {row.applyTime} 门店名称 {row.shopName}
  67. </div>
  68. </n-tag>
  69. </div>
  70. {row.orderRefundSkuList?.map(item => {
  71. return (
  72. <div class={'mb-3 h-80px flex items-center'}>
  73. <NImage src={item.pic} class="h-[80px] min-w-80px w-[80px]" lazy />
  74. <div class={'ml12px flex-1'}>
  75. <div class={'w-full flex items-center justify-between'}>
  76. <div class={'w200px'}>
  77. <n-ellipsis class={'w250px'}>
  78. <span class={'w-full text-left text-15px font-semibold'}>{item.skuName}</span>
  79. </n-ellipsis>
  80. </div>
  81. <div class={'w150px pl30px text-left'}>
  82. <div class={'text-16px font-semibold'}>¥{item.skuPrice} </div>
  83. </div>
  84. </div>
  85. <div class={'w-full flex items-center justify-between'}>
  86. <div class={'w200px text-gray'}>规格:{item.spec || '--'} </div>
  87. <div class={'w150px pl30px text-left text-gray'}>x{item.productCount} </div>
  88. </div>
  89. </div>
  90. </div>
  91. );
  92. })}
  93. </div>
  94. );
  95. }
  96. },
  97. {
  98. key: 'deptName',
  99. title: '单价(元)/数量',
  100. align: 'center',
  101. width: 100
  102. },
  103. {
  104. key: 'actualTotal',
  105. title: '实付金额(元)',
  106. align: 'center',
  107. width: 120,
  108. render: row => {
  109. return (
  110. <div class={'mt7'}>
  111. <div class={'text-16px text-#ff0000 font-semibold'}>{row.actualTotal} 元</div>
  112. <div>共 {row.goodsTotalCount} 件</div>
  113. </div>
  114. );
  115. }
  116. },
  117. {
  118. key: 'refundAmount',
  119. title: '退款金额',
  120. align: 'center',
  121. width: 120,
  122. render: row => {
  123. return (
  124. <div class={'mt7'}>
  125. <div class={'text-16px text-#ff0000 font-semibold'}>{row.actualTotal} 元</div>
  126. <div>共 {row.goodsTotalCount} 件</div>
  127. </div>
  128. );
  129. }
  130. },
  131. {
  132. title: '商品状况',
  133. key: 'goods',
  134. width: 220,
  135. align: 'center',
  136. render: row => {
  137. const TypeText = ['仅退款', '退款退货', '差价退款'];
  138. return (
  139. <div class={'mt7'}>
  140. <div class={'text-16px font-semibold'}>{TypeText[Number(row.applyType) - 1] || '未知状况'} </div>
  141. <div class={'text-#ff0000'}> {row.buyerReason} </div>
  142. </div>
  143. );
  144. }
  145. },
  146. {
  147. key: 'status',
  148. title: '订单状态',
  149. align: 'center',
  150. width: 220,
  151. render: row => {
  152. const statusKey = row.hbOrderStatus as keyof typeof orderStatus;
  153. const statusText = orderStatus[statusKey] || '未知状态';
  154. return <NTag class={'mt7'}>{statusText}</NTag>;
  155. }
  156. },
  157. {
  158. key: 'createTime',
  159. title: '售后状态',
  160. align: 'center',
  161. width: 160,
  162. render: row => {
  163. const statusKey = row.returnMoneySts as keyof typeof refundStatus;
  164. const statusText = refundStatus[statusKey] || '暂无售后';
  165. return <NTag class={'mt7'}>{statusText}</NTag>;
  166. }
  167. },
  168. {
  169. key: 'operate',
  170. title: $t('common.operate'),
  171. align: 'center',
  172. width: 130,
  173. fixed: 'right',
  174. render: row => {
  175. return (
  176. <div class={'mt7'}>
  177. <n-button size="small" type="primary" quaternary onClick={() => handleOpenMoadl(row)}>
  178. 查看订单
  179. </n-button>
  180. </div>
  181. );
  182. }
  183. }
  184. ]
  185. });
  186. function handleOpenMoadl(row: Api.delivery.OrderRefund) {
  187. if (!row.orderNumber) {
  188. window.$message?.error('订单异常');
  189. return;
  190. }
  191. orderMoadl.value?.open(String(row.orderNumber));
  192. }
  193. async function getNums() {
  194. const { data: keyData } = await fetchGetAfterSalesStatusNum({ ...getFieldsValue() });
  195. if (!keyData) return;
  196. const orderStatusList = [
  197. {
  198. label: '全部',
  199. value: 0,
  200. key: 'allCount'
  201. },
  202. {
  203. label: '买家申请',
  204. value: refundEnum.BUYER_APPLY,
  205. key: 'sellerApplyCount'
  206. },
  207. {
  208. label: '卖家拒绝',
  209. value: refundEnum.SELLER_REFUSE,
  210. key: 'sellerAcceptCount'
  211. },
  212. {
  213. label: '买家发货',
  214. value: refundEnum.BUYER_DELIVERY,
  215. key: 'sellerAcceptCount'
  216. },
  217. {
  218. label: '卖家接受',
  219. value: refundEnum.SELLER_AGREE,
  220. key: 'buyerDeliveryCount'
  221. },
  222. {
  223. label: '退款成功',
  224. value: refundEnum.REFUND_SUCCESS,
  225. key: 'refundCompleteCount'
  226. },
  227. {
  228. label: '撤回申请',
  229. value: refundEnum.REVOKE_APPLY,
  230. key: 'withdrawApplyCount'
  231. }
  232. ];
  233. const updatedOrderStatusList = orderStatusList.map(item => {
  234. if (Object.hasOwn(keyData, item.key)) {
  235. return {
  236. ...item,
  237. num: keyData[item.key]
  238. };
  239. }
  240. return item;
  241. });
  242. statusList.value = updatedOrderStatusList;
  243. }
  244. getNums();
  245. watch(
  246. () => [activeTab.value],
  247. () => {
  248. searchParams.current = 1;
  249. getData();
  250. }
  251. );
  252. function handleSearch() {
  253. const form = getFieldsValue();
  254. if (form.createTime) {
  255. form.startTime = form.createTime[0];
  256. form.endTime = form.createTime[1];
  257. delete form.createTime;
  258. }
  259. searchForm.value = form;
  260. getData();
  261. getNums();
  262. }
  263. function handleReset() {
  264. searchForm.value = getFieldsValue();
  265. getData();
  266. }
  267. async function handleCopy(No: string) {
  268. await copyTextToClipboard(No);
  269. }
  270. </script>
  271. <template>
  272. <div class="flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
  273. <NCard :bordered="false" size="small">
  274. <NCollapse display-directive="show">
  275. <NCollapseItem title="搜索">
  276. <BasicForm @register-form="registerSearchForm" @submit="handleSearch" @reset="handleReset" />
  277. </NCollapseItem>
  278. </NCollapse>
  279. </NCard>
  280. <NCard title="售后列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
  281. <NTabs v-model:value="activeTab" type="line" animated class="mb-16px h-full">
  282. <NTabPane
  283. v-for="item in statusList"
  284. :key="item.value"
  285. display-directive="show"
  286. :name="item.value"
  287. :tab="`${item.label}(${item.num})`"
  288. >
  289. <NDataTable
  290. v-model:checked-row-keys="checkedRowKeys"
  291. :columns="columns"
  292. :data="data"
  293. size="small"
  294. :flex-height="!appStore.isMobile"
  295. :scroll-x="1800"
  296. :loading="loading"
  297. :row-key="row => row.refundId"
  298. remote
  299. class="sm:h-full"
  300. :pagination="mobilePagination"
  301. />
  302. </NTabPane>
  303. </NTabs>
  304. <NormalMoadl ref="orderMoadl" @finish="(getData, getNums)"></NormalMoadl>
  305. </NCard>
  306. </div>
  307. </template>
  308. <style scoped lang="scss">
  309. :deep(.n-tabs-pane-wrapper) {
  310. height: 100%;
  311. }
  312. :deep(.n-tab-pane) {
  313. height: 100%;
  314. }
  315. </style>