|
|
@@ -0,0 +1,466 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { h, ref, reactive, shallowRef, onMounted, toRaw, computed } from 'vue';
|
|
|
+import EditShipment from '@/order/EditShipment.vue';
|
|
|
+import OrderDetail from '@/service/OrderDetail.vue';
|
|
|
+import LogisticsDetails from '@/order/LogisticsDetails.vue';
|
|
|
+import Negotiations from '@/order/Negotiations.vue';
|
|
|
+import SeeAmount from '@/order/SeeAmount.vue';
|
|
|
+import Amount from '@/order/Amount.vue';
|
|
|
+import Review from '@/order/Review.vue';
|
|
|
+import type { ShipmentModel, ShipmentQuery } from '@/model/order.model';
|
|
|
+import { getAllSupplierMethod } from '@/request/api/care.api';
|
|
|
+defineOptions({
|
|
|
+ name: 'ShipmentPage',
|
|
|
+});
|
|
|
+import { receiptStatus, receiptType } from '@/model/options';
|
|
|
+import dayjs from 'dayjs';
|
|
|
+// 接口数据
|
|
|
+import {
|
|
|
+ getShipmentListMethod,
|
|
|
+} from '@/request/api/order.api';
|
|
|
+import { usePagination } from 'alova/client';
|
|
|
+
|
|
|
+import {
|
|
|
+ type VxeFormListeners,
|
|
|
+ type VxeFormProps,
|
|
|
+ VxeUI,
|
|
|
+} from 'vxe-pc-ui';
|
|
|
+import type {
|
|
|
+ VxeGridInstance,
|
|
|
+ VxeGridListeners,
|
|
|
+ VxeGridProps,
|
|
|
+} from 'vxe-table';
|
|
|
+
|
|
|
+
|
|
|
+const model = shallowRef<ShipmentQuery>();
|
|
|
+const searchFormProps = reactive<VxeFormProps<ShipmentQuery>>({
|
|
|
+ titleWidth: 100,
|
|
|
+ titleAlign: 'right',
|
|
|
+ titleColon: true,
|
|
|
+ data: {
|
|
|
+ receiptStatus: '0', // 默认待发货
|
|
|
+ },
|
|
|
+ items: [
|
|
|
+ {
|
|
|
+ field: 'orderNo',
|
|
|
+ title: '订单编号',
|
|
|
+ span: 4,
|
|
|
+ itemRender: { name: 'VxeInput', props: { placeholder: '请输入' } },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: 'receiptStatus',
|
|
|
+ title: '收货人',
|
|
|
+ span: 4,
|
|
|
+
|
|
|
+ itemRender: {
|
|
|
+ name: 'VxeSelect',
|
|
|
+ options: receiptStatus,
|
|
|
+ props: { placeholder: '请选择', clearable: true },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ field: 'receiptType',
|
|
|
+ title: '退款状态',
|
|
|
+ span: 4,
|
|
|
+ itemRender: {
|
|
|
+ name: 'VxeSelect',
|
|
|
+ options: receiptType,
|
|
|
+ props: { placeholder: '请选择' },
|
|
|
+ },
|
|
|
+ },
|
|
|
+
|
|
|
+ {
|
|
|
+ span: 8,
|
|
|
+ itemRender: {
|
|
|
+ name: 'VxeButtonGroup',
|
|
|
+ options: [
|
|
|
+ { name: 'submits', type: 'submit', content: '查询', status: 'primary' },
|
|
|
+ { name: 'reset', type: 'reset', content: '重置' },
|
|
|
+ ],
|
|
|
+ events: {},
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+});
|
|
|
+const searchFormEmits: VxeFormListeners<ShipmentQuery> = {
|
|
|
+ submit({ data }) {
|
|
|
+ model.value = {
|
|
|
+ ...data,
|
|
|
+ payTimeStart: payTimeStart.value ? dayjs(payTimeStart.value).format('YYYY-MM-DD HH:mm:ss') : '',
|
|
|
+ payTimeEnd: payTimeEnd.value ? dayjs(payTimeEnd.value).format('YYYY-MM-DD HH:mm:ss') : '',
|
|
|
+ } as any;
|
|
|
+ },
|
|
|
+ // 重置
|
|
|
+ reset({ data }) {
|
|
|
+ model.value = { ...data } as any;
|
|
|
+ payTimeStart.value = '';
|
|
|
+ payTimeEnd.value = '';
|
|
|
+ },
|
|
|
+};
|
|
|
+const processedData = ref<ShipmentModel[]>([]);
|
|
|
+const orderGroupMap = ref<Map<string, { startIndex: number; count: number; rows: ShipmentModel[] }>>(new Map());
|
|
|
+
|
|
|
+function getGroupKey(item: ShipmentModel): string {
|
|
|
+ const orderNo = item.orderNo || '';
|
|
|
+ const supplierId = item.conditioningProgramSupplierId || '';
|
|
|
+ return `${orderNo}_${supplierId}`;
|
|
|
+}
|
|
|
+
|
|
|
+function processDataForMerge(data: ShipmentModel[]) {
|
|
|
+ const grouped = new Map<string, ShipmentModel[]>();
|
|
|
+ const result: ShipmentModel[] = [];
|
|
|
+
|
|
|
+ data.forEach((item) => {
|
|
|
+ const groupKey = getGroupKey(item);
|
|
|
+ if (!grouped.has(groupKey)) {
|
|
|
+ grouped.set(groupKey, []);
|
|
|
+ }
|
|
|
+ grouped.get(groupKey)!.push(item);
|
|
|
+ });
|
|
|
+
|
|
|
+ orderGroupMap.value.clear();
|
|
|
+ let currentIndex = 0;
|
|
|
+
|
|
|
+ grouped.forEach((rows, groupKey) => {
|
|
|
+ const count = rows.length;
|
|
|
+ orderGroupMap.value.set(groupKey, {
|
|
|
+ startIndex: currentIndex,
|
|
|
+ count,
|
|
|
+ rows,
|
|
|
+ });
|
|
|
+ result.push(...rows);
|
|
|
+ currentIndex += count;
|
|
|
+ });
|
|
|
+
|
|
|
+ processedData.value = result;
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+const gridRef = ref<VxeGridInstance<ShipmentModel>>();
|
|
|
+const gridOptions = reactive<VxeGridProps<ShipmentModel>>({
|
|
|
+ id: 'tag-list',
|
|
|
+ border: true,
|
|
|
+ showOverflow: true,
|
|
|
+ height: 'auto',
|
|
|
+ autoResize: false,
|
|
|
+ syncResize: true,
|
|
|
+ scrollY: { enabled: false },
|
|
|
+ spanMethod: ({ row, column, rowIndex }: any) => {
|
|
|
+ const groupKey = getGroupKey(row);
|
|
|
+ const groupInfo = orderGroupMap.value.get(groupKey);
|
|
|
+
|
|
|
+ if (!groupInfo) return { rowspan: 1, colspan: 1 };
|
|
|
+
|
|
|
+ const { startIndex, count } = groupInfo;
|
|
|
+ const isFirstRow = rowIndex === startIndex;
|
|
|
+
|
|
|
+ if (column.field === 'orderNo') {
|
|
|
+ return isFirstRow ? { rowspan: count, colspan: 1 } : { rowspan: 0, colspan: 0 };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (column.field === 'action') {
|
|
|
+ return isFirstRow ? { rowspan: count, colspan: 1 } : { rowspan: 0, colspan: 0 };
|
|
|
+ }
|
|
|
+
|
|
|
+ return { rowspan: 1, colspan: 1 };
|
|
|
+ },
|
|
|
+ rowConfig: {
|
|
|
+ isHover: true,
|
|
|
+ isCurrent: true,
|
|
|
+ },
|
|
|
+ toolbarConfig: {
|
|
|
+ custom: true,
|
|
|
+ zoom: true,
|
|
|
+ slots: {
|
|
|
+ // buttons: 'handle',
|
|
|
+ tools: 'toolbar-extra',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ columnConfig: {
|
|
|
+ resizable: true,
|
|
|
+ },
|
|
|
+ customConfig: {
|
|
|
+ storage: true,
|
|
|
+ },
|
|
|
+ columns: [
|
|
|
+ { type: 'seq', width: 70, fixed: 'left' },
|
|
|
+ { field: 'orderNo', title: '订单编号', slots: { default: 'orderNoCell' } },
|
|
|
+ { field: 'payTime', title: '售后类型' },
|
|
|
+ { field: 'totalMeasure', title: '退款更新时间' },
|
|
|
+ { field: 'conditioningProgramName', title: '项目名称' },
|
|
|
+ { field: 'totalMeasure', title: '商品类型' },
|
|
|
+ { field: 'totalPrice', title: '总价(元)' },
|
|
|
+ { field: 'pricingUnit', title: '收货人' },
|
|
|
+ { field: 'unitPrice', title: '退款状态' },
|
|
|
+ { field: 'receiptStatus', title: '物流状态', slots: { default: 'progressCell' } },
|
|
|
+ { field: 'receiptType', title: '核销情况', slots: { default: 'shipmentTypeCell' } },
|
|
|
+ { field: 'conditioningProgramSupplierName', title: '退款金额' },
|
|
|
+ { field: 'conditioningProgramSupplierName', title: '退款原因' },
|
|
|
+ { field: 'conditioningProgramSupplierName', title: '退款说明', slots: { default: 'refundReasonCell' } },
|
|
|
+ {
|
|
|
+ field: 'action',
|
|
|
+ title: '操作',
|
|
|
+ align: 'center',
|
|
|
+ width: 200,
|
|
|
+ showOverflow: false,
|
|
|
+ slots: { default: 'action' },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ data: [],
|
|
|
+});
|
|
|
+const gridEvents: VxeGridListeners = {};
|
|
|
+
|
|
|
+const { loading, page, pageSize, total, onSuccess, refresh } = usePagination(
|
|
|
+ (page, size) => getShipmentListMethod(page, size, model.value),
|
|
|
+ {
|
|
|
+ initialData: { data: [], total: 0 },
|
|
|
+ initialPage: 1,
|
|
|
+ initialPageSize: 100,
|
|
|
+ watchingStates: [model],
|
|
|
+ immediate: false,
|
|
|
+ }
|
|
|
+);
|
|
|
+onSuccess(({ data: { data } }) => {
|
|
|
+ const processed = processDataForMerge(data);
|
|
|
+ gridRef.value?.loadData(processed);
|
|
|
+});
|
|
|
+const supplierOptions = ref<{ label: string; value: string }[]>([]);
|
|
|
+const supplierArr = ref<any[]>([]);
|
|
|
+// 获取所有的供应商
|
|
|
+async function getSupplier(params: any) {
|
|
|
+ supplierOptions.value = [];
|
|
|
+ const res = (await getAllSupplierMethod(params)) as any[];
|
|
|
+ if (Array.isArray(res) && res.length > 0) {
|
|
|
+ supplierArr.value = res;
|
|
|
+ supplierOptions.value = res.map((item: any) => ({
|
|
|
+ label: item.name,
|
|
|
+ value: item.id,
|
|
|
+ }));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ model.value = toRaw(searchFormProps.data);
|
|
|
+ getSupplier({});
|
|
|
+});
|
|
|
+
|
|
|
+
|
|
|
+// 点击订单编号查看订单详情
|
|
|
+function serviceDetail(row?: ShipmentModel) {
|
|
|
+ if (!row) return;
|
|
|
+ const types = 'record';
|
|
|
+ VxeUI.modal.open({
|
|
|
+ id: 'servicePackageDetail-modal',
|
|
|
+ title: '调养记录详情',
|
|
|
+ height: window.innerHeight,
|
|
|
+ width: window.innerWidth,
|
|
|
+ fullscreen: true,
|
|
|
+ escClosable: true,
|
|
|
+ destroyOnClose: true,
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(OrderDetail, {
|
|
|
+ data: {
|
|
|
+ ...row,
|
|
|
+ id: row.id,
|
|
|
+ types,
|
|
|
+ },
|
|
|
+ onVoidSubmit() {
|
|
|
+ refresh(page.value);
|
|
|
+ },
|
|
|
+ } as any);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function goShipment(row?: ShipmentModel) {
|
|
|
+ if (!row) return;
|
|
|
+
|
|
|
+ const isReview = row.receiptStatus === '0'; // 待发货 -> 审核
|
|
|
+ const isEditAmount = row.receiptStatus === '1'; // 已发货 -> 金额
|
|
|
+ if (isReview) {
|
|
|
+ VxeUI.modal.open({
|
|
|
+ title: '审核',
|
|
|
+ height: 900,
|
|
|
+ width: 1200,
|
|
|
+ escClosable: true,
|
|
|
+ destroyOnClose: true,
|
|
|
+ id: `review-modal`,
|
|
|
+ remember: true,
|
|
|
+ storage: true,
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(Review, {
|
|
|
+ data: row,
|
|
|
+ onSubmit: () => {
|
|
|
+ refresh(page.value);
|
|
|
+ VxeUI.modal.close('review-modal');
|
|
|
+ },
|
|
|
+ onClose: () => {
|
|
|
+ VxeUI.modal.close('review-modal');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 金额 or 查看金额
|
|
|
+ const title = isEditAmount ? '退款金额' : '退款金额查看';
|
|
|
+ const component = isEditAmount ? Amount : SeeAmount;
|
|
|
+
|
|
|
+ VxeUI.modal.open({
|
|
|
+ title,
|
|
|
+ height: 500,
|
|
|
+ width: 600,
|
|
|
+ escClosable: true,
|
|
|
+ destroyOnClose: true,
|
|
|
+ id: isEditAmount ? 'amount-edit-modal' : 'amount-see-modal',
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(component, {
|
|
|
+ data: row,
|
|
|
+ onSubmit: (amount: number) => {
|
|
|
+ console.log('Final Amount:', amount);
|
|
|
+ // TODO: API call to update amount
|
|
|
+ refresh(page.value);
|
|
|
+ VxeUI.modal.close('amount-edit-modal');
|
|
|
+ },
|
|
|
+ onClose: () => {
|
|
|
+ VxeUI.modal.close(isEditAmount ? 'amount-edit-modal' : 'amount-see-modal');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 查看物流详情
|
|
|
+function viewLogistics(row?: ShipmentModel) {
|
|
|
+ if (!row) return;
|
|
|
+ VxeUI.modal.open({
|
|
|
+ title: '物流详情',
|
|
|
+ height: 800,
|
|
|
+ width: 1000,
|
|
|
+ escClosable: true,
|
|
|
+ destroyOnClose: true,
|
|
|
+ id: 'logistics-modal',
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(LogisticsDetails, {
|
|
|
+ data: row
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function goAfterSale(row?: ShipmentModel) {
|
|
|
+ if (!row) return;
|
|
|
+ // TODO: Implement after sale details or history
|
|
|
+ console.log('goAfterSale', row);
|
|
|
+ VxeUI.modal.open({
|
|
|
+ title: '协商历史',
|
|
|
+ height: 800,
|
|
|
+ width: 1000,
|
|
|
+ escClosable: true,
|
|
|
+ destroyOnClose: true,
|
|
|
+ id: 'negotiations-modal',
|
|
|
+ slots: {
|
|
|
+ default() {
|
|
|
+ return h(Negotiations, {
|
|
|
+ data: row
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+const payTimeStart = ref<string>('');
|
|
|
+const payTimeEnd = ref<string>('');
|
|
|
+function disabledPayEndDate(current: any) {
|
|
|
+ if (!payTimeStart.value) return false;
|
|
|
+ return current && current < dayjs(payTimeStart.value);
|
|
|
+}
|
|
|
+</script>
|
|
|
+<template>
|
|
|
+ <div class="page-container flex flex-col">
|
|
|
+ <header class="flex-none mt-4">
|
|
|
+ <vxe-form v-bind="searchFormProps" v-on="searchFormEmits">
|
|
|
+ <template #payTimeRange>
|
|
|
+ <div class="date-range-container">
|
|
|
+ <a-date-picker v-model:value="payTimeStart" placeholder="请选择开始时间" style="flex: 1"
|
|
|
+ :show-time="{ format: 'HH:mm' }" />
|
|
|
+ <span class="date-separator">至</span>
|
|
|
+ <a-date-picker v-model:value="payTimeEnd" placeholder="请选择结束时间" style="flex: 1"
|
|
|
+ :disabledDate="disabledPayEndDate" :show-time="{ format: 'HH:mm' }" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </vxe-form>
|
|
|
+ </header>
|
|
|
+ <main class="flex-auto overflow-hidden">
|
|
|
+ <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
|
|
|
+ <template #orderNoCell="{ row }">
|
|
|
+ <span class="order-no-link" @click="serviceDetail(row)">{{ row.orderNo }}</span>
|
|
|
+ </template>
|
|
|
+ <template #progressCell="{ row }">
|
|
|
+ <span class="order-no-link" @click="viewLogistics(row)">{{ row.receiptStatus === '0' ? '待发货' : row.receiptStatus === '1' ? '已发货' : row.receiptStatus === '2' ?
|
|
|
+ '已收货' : '' }}</span>
|
|
|
+ </template>
|
|
|
+ <template #shipmentTypeCell="{ row }">
|
|
|
+ <div>{{ row.receiptType === '0' ? '配送' : row.receiptType === '1' ? '线下取货' : '' }}</div>
|
|
|
+ </template>
|
|
|
+ <template #refundReasonCell="{ row }">
|
|
|
+ <span class="order-no-link" @click="goAfterSale(row)">{{ row.conditioningProgramSupplierName }}</span>
|
|
|
+ </template>
|
|
|
+ <template #action="{ row }">
|
|
|
+ <vxe-button mode="text" status="primary" @click="goAfterSale(row)">
|
|
|
+ 协商历史
|
|
|
+ </vxe-button>
|
|
|
+ <vxe-button mode="text" status="primary" @click="goShipment(row)">
|
|
|
+ {{ row.receiptStatus === '0' ? '审核' : row.receiptStatus === '1' ? '金额' : '查看金额' }}
|
|
|
+ </vxe-button>
|
|
|
+ </template>
|
|
|
+ <template #toolbar-extra>
|
|
|
+ <vxe-button style="margin-right: 12px" icon="vxe-icon-repeat" circle @click="refresh(page)"></vxe-button>
|
|
|
+ </template>
|
|
|
+ </vxe-grid>
|
|
|
+ </main>
|
|
|
+ <footer class="flex-none">
|
|
|
+ <vxe-pager v-model:current-page="page" v-model:page-size="pageSize" :total="total" :layouts="[
|
|
|
+ 'Home',
|
|
|
+ 'PrevJump',
|
|
|
+ 'PrevPage',
|
|
|
+ 'Number',
|
|
|
+ 'NextPage',
|
|
|
+ 'NextJump',
|
|
|
+ 'End',
|
|
|
+ 'Sizes',
|
|
|
+ 'FullJump',
|
|
|
+ 'Total',
|
|
|
+ ]" />
|
|
|
+ </footer>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<style scoped lang="scss">
|
|
|
+.date-separator {
|
|
|
+ margin: 0 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-container {
|
|
|
+ padding: 0 24px;
|
|
|
+ max-height: var(--page-main-container);
|
|
|
+}
|
|
|
+
|
|
|
+.order-no-link {
|
|
|
+ color: #1890ff;
|
|
|
+ cursor: pointer;
|
|
|
+ text-decoration: none;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+ color: #40a9ff;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|