diff --git a/src/api/mall/trade/afterSale/index.ts b/src/api/mall/trade/afterSale/index.ts new file mode 100644 index 00000000..a109ee6b --- /dev/null +++ b/src/api/mall/trade/afterSale/index.ts @@ -0,0 +1,75 @@ +import request from '@/config/axios' + +export interface TradeAfterSaleVO { + id?: number | null // 售后编号,主键自增 + no?: string // 售后单号 + status?: number | null // 退款状态 + way?: number | null // 售后方式 + type?: number | null // 售后类型 + userId?: number | null // 用户编号 + applyReason?: string // 申请原因 + applyDescription?: string // 补充描述 + applyPicUrls?: string[] // 补充凭证图片 + orderId?: number | null // 交易订单编号 + orderNo?: string // 订单流水号 + orderItemId?: number | null // 交易订单项编号 + spuId?: number | null // 商品 SPU 编号 + spuName?: string // 商品 SPU 名称 + skuId?: number | null // 商品 SKU 编号 + properties?: ProductPropertiesVO[] // 属性数组 + picUrl?: string // 商品图片 + count?: number | null // 退货商品数量 + auditTime?: Date // 审批时间 + auditUserId?: number | null // 审批人 + auditReason?: string // 审批备注 + refundPrice?: number | null // 退款金额,单位:分。 + payRefundId?: number | null // 支付退款编号 + refundTime?: Date // 退款时间 + logisticsId?: number | null // 退货物流公司编号 + logisticsNo?: string // 退货物流单号 + deliveryTime?: Date // 退货时间 + receiveTime?: Date // 收货时间 + receiveReason?: string // 收货备注 +} + +export interface ProductPropertiesVO { + propertyId?: number | null // 属性的编号 + propertyName?: string // 属性的名称 + valueId?: number | null //属性值的编号 + valueName?: string // 属性值的名称 +} + +// 获得交易售后分页 +export const getAfterSalePage = async (params) => { + return await request.get({ url: `/trade/after-sale/page`, params }) +} + +// 获得交易售后详情 +export const getAfterSale = async (id: any) => { + return await request.get({ url: `/trade/after-sale/get-detail?id=${id}` }) +} + +// 同意售后 +export const agree = async (id: any) => { + return await request.put({ url: `/trade/after-sale/agree?id=${id}` }) +} + +// 拒绝售后 +export const disagree = async (data: any) => { + return await request.put({ url: `/trade/after-sale/disagree`, data }) +} + +// 确认收货 +export const receive = async (id: any) => { + return await request.put({ url: `/trade/after-sale/receive?id=${id}` }) +} + +// 拒绝收货 +export const refuse = async (id: any) => { + return await request.put({ url: `/trade/after-sale/refuse?id=${id}` }) +} + +// 确认退款 +export const refund = async (id: any) => { + return await request.put({ url: `/trade/after-sale/refund?id=${id}` }) +} diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 504a6e9e..c6c95e80 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -195,23 +195,6 @@ const remainingRouter: AppRouteRecordRaw[] = [ noTagsView: true } }, - { - path: '/trade/order', - component: Layout, - name: 'order', - meta: { - hidden: true - }, - children: [ - { - path: 'detail', - name: 'TradeOrderDetail', - // component: () => import('@/views/mall/trade/order/tradeOrderDetail.vue'), - component: () => import('@/views/mall/trade/order/detail/index.vue'), - meta: { title: '订单详情', hidden: true } - } - ] - }, { path: '/403', component: () => import('@/views/Error/403.vue'), @@ -357,7 +340,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ children: [ { path: 'spu/add', - component: () => import('@/views/mall/product/spu/addForm.vue'), + component: () => import('@/views/mall/product/spu/form/index.vue'), name: 'ProductSpuAdd', meta: { noCache: true, @@ -370,7 +353,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ }, { path: 'spu/edit/:spuId(\\d+)', - component: () => import('@/views/mall/product/spu/addForm.vue'), + component: () => import('@/views/mall/product/spu/form/index.vue'), name: 'ProductSpuEdit', meta: { noCache: true, @@ -383,7 +366,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ }, { path: 'spu/detail/:spuId(\\d+)', - component: () => import('@/views/mall/product/spu/addForm.vue'), + component: () => import('@/views/mall/product/spu/form/index.vue'), name: 'ProductSpuDetail', meta: { noCache: true, @@ -410,18 +393,24 @@ const remainingRouter: AppRouteRecordRaw[] = [ ] }, { - path: '/trade/order', + path: '/trade', component: Layout, - name: 'Detail', + name: 'Order', meta: { hidden: true }, children: [ { - path: 'detail/:orderId(\\d+)', + path: 'order/detail/:orderId(\\d+)', component: () => import('@/views/mall/trade/order/detail/index.vue'), - name: 'TradeOrderDetailForm', + name: 'TradeOrderDetail', meta: { title: '订单详情', icon: '', activeMenu: '/trade/trade/order' } + }, + { + path: 'after-sale/detail/:orderId(\\d+)', + component: () => import('@/views/mall/trade/afterSale/detail/index.vue'), + name: 'TradeAfterSaleDetail', + meta: { title: '退款详情', icon: '', activeMenu: '/trade/trade/after-sale' } } ] }, diff --git a/src/views/mall/product/spu/components/index.ts b/src/views/mall/product/spu/components/index.ts index 405dfde5..8f793c51 100644 --- a/src/views/mall/product/spu/components/index.ts +++ b/src/views/mall/product/spu/components/index.ts @@ -1,12 +1,5 @@ -import BasicInfoForm from './BasicInfoForm.vue' -import DescriptionForm from './DescriptionForm.vue' -import OtherSettingsForm from './OtherSettingsForm.vue' -import ProductAttributes from './ProductAttributes.vue' -import ProductPropertyAddForm from './ProductPropertyAddForm.vue' import SkuList from './SkuList.vue' -import { Spu } from '@/api/mall/product/spu' - interface PropertyAndValues { id: number name: string @@ -29,42 +22,4 @@ interface RuleConfig { message: string } -/** - * 获得商品的规格列表 - * - * @param spu - * @return PropertyAndValues 规格列表 - */ -const getPropertyList = (spu: Spu): PropertyAndValues[] => { - // 直接拿返回的 skus 属性逆向生成出 propertyList - const properties: PropertyAndValues[] = [] - // 只有是多规格才处理 - if (spu.specType) { - spu.skus?.forEach((sku) => { - sku.properties?.forEach(({ propertyId, propertyName, valueId, valueName }) => { - // 添加属性 - if (!properties?.some((item) => item.id === propertyId)) { - properties.push({ id: propertyId!, name: propertyName!, values: [] }) - } - // 添加属性值 - const index = properties?.findIndex((item) => item.id === propertyId) - if (!properties[index].values?.some((value) => value.id === valueId)) { - properties[index].values?.push({ id: valueId!, name: valueName! }) - } - }) - }) - } - return properties -} - -export { - BasicInfoForm, - DescriptionForm, - OtherSettingsForm, - ProductAttributes, - ProductPropertyAddForm, - SkuList, - getPropertyList, - PropertyAndValues, - RuleConfig -} +export { SkuList, PropertyAndValues, RuleConfig } diff --git a/src/views/mall/product/spu/components/BasicInfoForm.vue b/src/views/mall/product/spu/form/BasicInfoForm.vue similarity index 88% rename from src/views/mall/product/spu/components/BasicInfoForm.vue rename to src/views/mall/product/spu/form/BasicInfoForm.vue index 501295b4..678b564d 100644 --- a/src/views/mall/product/spu/components/BasicInfoForm.vue +++ b/src/views/mall/product/spu/form/BasicInfoForm.vue @@ -175,13 +175,17 @@ import { propTypes } from '@/utils/propTypes' import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree' import { createImageViewer } from '@/components/ImageViewer' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { UploadImg, UploadImgs } from '@/components/UploadFile' -import { getPropertyList, ProductAttributes, ProductPropertyAddForm, SkuList } from './index' +import { PropertyAndValues, SkuList } from '@/views/mall/product/spu/components/index.ts' +import ProductAttributes from './ProductAttributes.vue' +import ProductPropertyAddForm from './ProductPropertyAddForm.vue' import { basicInfoSchema } from './spu.data' import type { Spu } from '@/api/mall/product/spu' import * as ProductCategoryApi from '@/api/mall/product/category' -import { getSimpleBrandList } from '@/api/mall/product/brand' -import { getSimpleTemplateList } from '@/api/mall/trade/delivery/expressTemplate/index' +import * as ProductBrandApi from '@/api/mall/product/brand' +import * as ExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate' + +defineOptions({ name: 'ProductSpuBasicInfoForm' }) + // ====== 商品详情相关操作 ====== const { allSchemas } = useCrudSchemas(basicInfoSchema) /** 商品图预览 */ @@ -198,9 +202,36 @@ const imagePreview = (args) => { urlList }) } -// ====== end ====== -defineOptions({ name: 'ProductSpuBasicInfoForm' }) +/** + * 获得商品的规格列表 + * + * @param spu + * @return PropertyAndValues 规格列表 + */ +const getPropertyList = (spu: Spu): PropertyAndValues[] => { + // 直接拿返回的 skus 属性逆向生成出 propertyList + const properties: PropertyAndValues[] = [] + // 只有是多规格才处理 + if (spu.specType) { + spu.skus?.forEach((sku) => { + sku.properties?.forEach(({ propertyId, propertyName, valueId, valueName }) => { + // 添加属性 + if (!properties?.some((item) => item.id === propertyId)) { + properties.push({ id: propertyId!, name: propertyName!, values: [] }) + } + // 添加属性值 + const index = properties?.findIndex((item) => item.id === propertyId) + if (!properties[index].values?.some((value) => value.id === valueId)) { + properties[index].values?.push({ id: valueId!, name: valueName! }) + } + }) + }) + } + return properties +} + +// ====== end ====== const message = useMessage() // 消息弹窗 @@ -346,8 +377,8 @@ onMounted(async () => { const data = await ProductCategoryApi.getCategoryList({}) categoryList.value = handleTree(data, 'id', 'parentId') // 获取商品品牌列表 - brandList.value = await getSimpleBrandList() + brandList.value = await ProductBrandApi.getSimpleBrandList() // 获取运费模版 - deliveryTemplateList.value = await getSimpleTemplateList() + deliveryTemplateList.value = await ExpressTemplateApi.getSimpleTemplateList() }) </script> diff --git a/src/views/mall/product/spu/components/DescriptionForm.vue b/src/views/mall/product/spu/form/DescriptionForm.vue similarity index 100% rename from src/views/mall/product/spu/components/DescriptionForm.vue rename to src/views/mall/product/spu/form/DescriptionForm.vue diff --git a/src/views/mall/product/spu/components/OtherSettingsForm.vue b/src/views/mall/product/spu/form/OtherSettingsForm.vue similarity index 100% rename from src/views/mall/product/spu/components/OtherSettingsForm.vue rename to src/views/mall/product/spu/form/OtherSettingsForm.vue diff --git a/src/views/mall/product/spu/components/ProductAttributes.vue b/src/views/mall/product/spu/form/ProductAttributes.vue similarity index 100% rename from src/views/mall/product/spu/components/ProductAttributes.vue rename to src/views/mall/product/spu/form/ProductAttributes.vue diff --git a/src/views/mall/product/spu/components/ProductPropertyAddForm.vue b/src/views/mall/product/spu/form/ProductPropertyAddForm.vue similarity index 98% rename from src/views/mall/product/spu/components/ProductPropertyAddForm.vue rename to src/views/mall/product/spu/form/ProductPropertyAddForm.vue index 243cd7a0..a3e09fef 100644 --- a/src/views/mall/product/spu/components/ProductPropertyAddForm.vue +++ b/src/views/mall/product/spu/form/ProductPropertyAddForm.vue @@ -80,6 +80,8 @@ const submitForm = async () => { if (res[0].values === null) { res[0].values = [] } + // 不需要属性值 + res[0].values = [] attributeList.value.push(res[0]) // 因为只用一个 } message.success(t('common.createSuccess')) diff --git a/src/views/mall/product/spu/addForm.vue b/src/views/mall/product/spu/form/index.vue similarity index 97% rename from src/views/mall/product/spu/addForm.vue rename to src/views/mall/product/spu/form/index.vue index 910bad4a..db5b0445 100644 --- a/src/views/mall/product/spu/addForm.vue +++ b/src/views/mall/product/spu/form/index.vue @@ -39,13 +39,12 @@ <script lang="ts" setup> import { cloneDeep } from 'lodash-es' import { useTagsViewStore } from '@/store/modules/tagsView' -import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components' -// 业务api import * as ProductSpuApi from '@/api/mall/product/spu' +import BasicInfoForm from './BasicInfoForm.vue' +import DescriptionForm from './DescriptionForm.vue' +import OtherSettingsForm from './OtherSettingsForm.vue' import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils' -// TODO @芋艿:后续稍微调整下; - defineOptions({ name: 'ProductSpuForm' }) const { t } = useI18n() // 国际化 diff --git a/src/views/mall/product/spu/components/spu.data.ts b/src/views/mall/product/spu/form/spu.data.ts similarity index 100% rename from src/views/mall/product/spu/components/spu.data.ts rename to src/views/mall/product/spu/form/spu.data.ts diff --git a/src/views/mall/trade/afterSale/detail/index.vue b/src/views/mall/trade/afterSale/detail/index.vue new file mode 100644 index 00000000..7802a805 --- /dev/null +++ b/src/views/mall/trade/afterSale/detail/index.vue @@ -0,0 +1,245 @@ +<template> + <ContentWrap> + <!-- 订单信息 --> + <el-descriptions title="订单信息"> + <el-descriptions-item label="订单号: ">{{ formData.orderNo }}</el-descriptions-item> + <el-descriptions-item label="配送方式: "> + <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="formData.order.deliveryType" /> + </el-descriptions-item> + <!-- TODO 营销活动待实现 --> + <el-descriptions-item label="订单类型: "> + <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.order.type" /> + </el-descriptions-item> + <el-descriptions-item label="收货人: "> + {{ formData.order.receiverName }} + </el-descriptions-item> + <el-descriptions-item label="买家留言: "> + {{ formData.order.userRemark }} + </el-descriptions-item> + <el-descriptions-item label="订单来源: "> + <dict-tag :type="DICT_TYPE.TERMINAL" :value="formData.order.terminal" /> + </el-descriptions-item> + <el-descriptions-item label="联系电话: "> + {{ formData.order.receiverMobile }} + </el-descriptions-item> + <el-descriptions-item label="商家备注: ">{{ formData.order.remark }}</el-descriptions-item> + <el-descriptions-item label="支付单号: "> + {{ formData.order.payOrderId }} + </el-descriptions-item> + <el-descriptions-item label="付款方式: "> + <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.order.payChannelCode" /> + </el-descriptions-item> + <!-- TODO 芋艿:待实现:跳转会员 --> + <!-- <el-descriptions-item label="买家: ">{{ formData.user.nickname }}</el-descriptions-item> --> + </el-descriptions> + + <!-- 售后信息 --> + <el-descriptions title="售后信息"> + <el-descriptions-item label="退款编号: ">{{ formData.no }}</el-descriptions-item> + <el-descriptions-item label="申请时间: "> + {{ formatDate(formData.auditTime) }} + </el-descriptions-item> + <el-descriptions-item label="售后类型: "> + <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_TYPE" :value="formData.type" /> + </el-descriptions-item> + <el-descriptions-item label="售后方式: "> + <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="formData.way" /> + </el-descriptions-item> + <el-descriptions-item label="退款金额: "> + {{ floatToFixed2(formData.refundPrice) }} + </el-descriptions-item> + <el-descriptions-item label="退款原因: ">{{ formData.applyReason }}</el-descriptions-item> + <el-descriptions-item label="补充描述: "> + {{ formData.applyDescription }} + </el-descriptions-item> + <el-descriptions-item label="凭证图片: "> + <el-image + v-for="(item, index) in formData.applyPicUrls" + :key="index" + :src="item.url" + class="w-60px h-60px mr-10px" + @click="imagePreview(formData.applyPicUrls)" + /> + </el-descriptions-item> + </el-descriptions> + + <!-- 退款状态 --> + <el-descriptions :column="1" title="退款状态"> + <el-descriptions-item label="退款状态: "> + <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="formData.status" /> + </el-descriptions-item> + <el-descriptions-item label-class-name="no-colon"> + <el-button v-if="formData.status === 10" type="primary" @click="agree">同意售后</el-button> + <el-button v-if="formData.status === 10" type="primary" @click="disagree"> + 拒绝售后 + </el-button> + <el-button v-if="formData.status === 30" type="primary" @click="receive"> + 确认收货 + </el-button> + <el-button v-if="formData.status === 30" type="primary" @click="refuse">拒绝收货</el-button> + <el-button v-if="formData.status === 40" type="primary" @click="refund">确认退款</el-button> + </el-descriptions-item> + <el-descriptions-item> + <template #label><span style="color: red">提醒: </span></template> + 如果未发货,请点击同意退款给买家。<br /> + 如果实际已发货,请主动与买家联系。<br /> + 如果订单整体退款后,优惠券和余额会退还给买家. + </el-descriptions-item> + </el-descriptions> + + <!-- 商品信息 --> + <el-descriptions title="商品信息"> + <el-descriptions-item labelClassName="no-colon"> + <el-row :gutter="20"> + <el-col :span="15"> + <el-table :data="formData.items" border> + <el-table-column label="商品" prop="spuName" width="auto"> + <template #default="{ row }"> + {{ row.spuName }} + <el-tag v-for="property in row.properties" :key="property.propertyId"> + {{ property.propertyName }}: {{ property.valueName }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="商品原价" prop="price" width="150"> + <template #default="{ row }">{{ floatToFixed2(row.price) }}元</template> + </el-table-column> + <el-table-column label="数量" prop="count" width="100" /> + <el-table-column label="合计" prop="payPrice" width="150"> + <template #default="{ row }">{{ floatToFixed2(row.payPrice) }}元</template> + </el-table-column> + <el-table-column label="售后状态" prop="afterSaleStatus" width="120"> + <template #default="{ row }"> + <dict-tag + :type="DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS" + :value="row.afterSaleStatus" + /> + </template> + </el-table-column> + </el-table> + </el-col> + <el-col :span="10" /> + </el-row> + </el-descriptions-item> + </el-descriptions> + <!-- 售后信息 TODO @puhui999:需要接入 --> + <el-descriptions title="售后日志" /> + </ContentWrap> + + <!-- 各种操作的弹窗 --> + <UpdateAuditReasonForm ref="updateAuditReasonFormRef" @success="getDetail" /> +</template> +<script lang="ts" setup> +import * as AfterSaleApi from '@/api/mall/trade/afterSale/index' +import { floatToFixed2 } from '@/utils' +import { DICT_TYPE } from '@/utils/dict' +import { formatDate } from '@/utils/formatTime' +import UpdateAuditReasonForm from '@/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue' +import { createImageViewer } from '@/components/ImageViewer' +import { isArray } from '@/utils/is' + +defineOptions({ name: 'TradeOrderDetailForm' }) + +const message = useMessage() // 消息弹窗 +const { params } = useRoute() // 查询参数 +const formData = ref({ + order: {} +}) +const updateAuditReasonFormRef = ref() // 拒绝售后表单 Ref + +/** 获得详情 */ +const getDetail = async () => { + const id = params.orderId as unknown as number + if (id) { + formData.value = await AfterSaleApi.getAfterSale(id) + } +} +/** + * 同意售后 + */ +const agree = () => { + message.confirm('是否同意售后?').then(() => { + AfterSaleApi.agree(formData.value.id) + }) +} +/** + * 拒绝售后 + */ +const disagree = () => { + updateAuditReasonFormRef.value?.open(formData.value) +} +/** + * 确认收货 + */ +const receive = () => { + message.confirm('是否确认收货?').then(() => { + AfterSaleApi.receive(formData.value.id) + }) +} +/** + * 拒绝收货 + */ +const refuse = () => { + message.confirm('是否拒绝收货?').then(() => { + AfterSaleApi.refuse(formData.value.id) + }) +} +/** + * 确认退款 + */ +const refund = () => { + message.confirm('是否确认退款?').then(() => { + AfterSaleApi.refund(formData.value.id) + }) +} +/** 图片预览 */ +const imagePreview = (args) => { + const urlList = [] + if (isArray(args)) { + args.forEach((item) => { + urlList.push(item.url) + }) + } else { + urlList.push(args) + } + createImageViewer({ + urlList + }) +} +onMounted(async () => { + await getDetail() +}) +</script> +<style lang="scss" scoped> +:deep(.el-descriptions) { + &:not(:nth-child(1)) { + margin-top: 20px; + } + + .el-descriptions__title { + display: flex; + align-items: center; + + &::before { + display: inline-block; + width: 3px; + height: 20px; + margin-right: 10px; + background-color: #409eff; + content: ''; + } + } + + .el-descriptions-item__container { + margin: 0 10px; + + .no-colon { + margin: 0; + + &::after { + content: ''; + } + } + } +} +</style> diff --git a/src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue b/src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue new file mode 100644 index 00000000..1fa2b0f5 --- /dev/null +++ b/src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue @@ -0,0 +1,70 @@ +<template> + <Dialog v-model="dialogVisible" title="拒绝售后" width="45%"> + <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px"> + <el-form-item label="审批备注"> + <el-input + v-model="formData.auditReason" + :rows="3" + placeholder="请输入审批备注" + type="textarea" + /> + </el-form-item> + </el-form> + <template #footer> + <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script lang="ts" setup> +import * as AfterSaleApi from '@/api/mall/trade/afterSale/index' + +defineOptions({ name: 'AfterSaleDisagreeForm' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formData = ref({ + id: 0, // 售后订单编号 + auditReason: '' // 审批备注 +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (row: AfterSaleApi.TradeAfterSaleVO) => { + resetForm() + // 设置数据 + formData.value.id = row.id + formData.value.auditReason = row.auditReason + dialogVisible.value = true +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 提交请求 + formLoading.value = true + try { + const data = unref(formData) + await AfterSaleApi.disagree(data) + message.success(t('common.updateSuccess')) + dialogVisible.value = false + // 发送操作成功的事件 + emit('success', true) + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: 0, // 售后订单编号 + auditReason: '' // 审批备注 + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/mall/trade/afterSale/index.vue b/src/views/mall/trade/afterSale/index.vue new file mode 100644 index 00000000..d22e5295 --- /dev/null +++ b/src/views/mall/trade/afterSale/index.vue @@ -0,0 +1,268 @@ +<template> + <!-- 搜索 --> + <ContentWrap> + <el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="68px"> + <el-form-item label="商品名称" prop="spuName"> + <el-input + v-model="queryParams.spuName" + class="!w-280px" + clearable + placeholder="请输入商品 SPU 名称" + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="退款编号" prop="no"> + <el-input + v-model="queryParams.no" + class="!w-280px" + clearable + placeholder="请输入退款编号" + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="订单编号" prop="orderNo"> + <el-input + v-model="queryParams.orderNo" + class="!w-280px" + clearable + placeholder="请输入订单编号" + @keyup.enter="handleQuery" + /> + </el-form-item> + <el-form-item label="售后状态" prop="status"> + <el-select + v-model="queryParams.status" + class="!w-280px" + clearable + placeholder="请选择售后状态" + > + <el-option label="全部" value="0" /> + <el-option + v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="售后方式" prop="way"> + <el-select + v-model="queryParams.way" + class="!w-280px" + clearable + placeholder="请选择售后方式" + > + <el-option + v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_WAY)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="售后类型" prop="type"> + <el-select + v-model="queryParams.type" + class="!w-280px" + clearable + placeholder="请选择售后类型" + > + <el-option + v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-260px" + end-placeholder="自定义时间" + start-placeholder="自定义时间" + type="daterange" + value-format="YYYY-MM-DD HH:mm:ss" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"> + <Icon class="mr-5px" icon="ep:search" /> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon class="mr-5px" icon="ep:refresh" /> + 重置 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <ContentWrap> + <el-tabs v-model="queryParams.status" @tab-click="tabClick"> + <el-tab-pane + v-for="item in statusTabs" + :key="item.label" + :label="item.label" + :name="item.value" + /> + </el-tabs> + <!-- 列表 --> + <el-table v-loading="loading" :data="list"> + <el-table-column align="center" label="退款编号" min-width="200" prop="no" /> + <el-table-column align="center" label="订单编号" min-width="200" prop="orderNo"> + <template #default="{ row }"> + <el-button link type="primary" @click="openOrderDetail(row.orderId)"> + {{ row.orderNo }} + </el-button> + </template> + </el-table-column> + <el-table-column label="商品信息" min-width="600" prop="spuName"> + <template #default="{ row }"> + <div class="flex items-center"> + <el-image + :src="row.picUrl" + class="w-30px h-30px mr-10px" + @click="imagePreview(row.picUrl)" + /> + <span class="mr-10px">{{ row.spuName }}</span> + <el-tag v-for="property in row.properties" :key="property.propertyId" class="mr-10px"> + {{ property.propertyName }}: {{ property.valueName }} + </el-tag> + </div> + </template> + </el-table-column> + <el-table-column align="center" label="订单金额" prop="refundPrice"> + <template #default="scope"> + <span>{{ floatToFixed2(scope.row.refundPrice) }}元</span> + </template> + </el-table-column> + <!-- TODO 芋艿:未来要加个会员链接 --> + <el-table-column align="center" label="买家" prop="user.nickname" /> + <el-table-column align="center" label="申请时间" prop="createTime" width="180"> + <template #default="scope"> + <span>{{ formatDate(scope.row.createTime) }}</span> + </template> + </el-table-column> + <el-table-column align="center" label="售后状态"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="scope.row.status" /> + </template> + </el-table-column> + <el-table-column align="center" label="售后方式"> + <template #default="scope"> + <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="scope.row.way" /> + </template> + </el-table-column> + <el-table-column align="center" fixed="right" label="操作" width="160"> + <template #default="{ row }"> + <el-button link type="primary" @click="openAfterSaleDetail(row.id)">处理退款</el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + v-model:limit="queryParams.pageSize" + v-model:page="queryParams.pageNo" + :total="total" + @pagination="getList" + /> + </ContentWrap> +</template> +<script lang="ts" setup> +import * as AfterSaleApi from '@/api/mall/trade/afterSale/index' +import { DICT_TYPE, getDictOptions } from '@/utils/dict' +import { formatDate } from '@/utils/formatTime' +import { createImageViewer } from '@/components/ImageViewer' +import { TabsPaneContext } from 'element-plus' +import { cloneDeep } from 'lodash-es' +import { floatToFixed2 } from '@/utils' + +defineOptions({ name: 'TradeAfterSale' }) + +const { push } = useRouter() // 路由跳转 + +const loading = ref(true) // 列表的加载中 +const total = ref(0) // 列表的总页数 +const list = ref<AfterSaleApi.TradeAfterSaleVO[]>([]) // 列表的数据 +const statusTabs = ref([ + { + label: '全部', + value: '0' + } +]) +const queryFormRef = ref() // 搜索的表单 +// 查询参数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + no: null, + status: '0', + orderNo: null, + spuName: null, + createTime: [], + way: null, + type: null +}) +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = cloneDeep(queryParams) + // 处理掉全部的状态,不传就是全部 + if (data.status === '0') { + delete data.status + } + // 执行查询 + const res = (await AfterSaleApi.getAfterSalePage(data)) as AfterSaleApi.TradeAfterSaleVO[] + list.value = res.list + total.value = res.total + } finally { + loading.value = false + } +} +/** 搜索按钮操作 */ +const handleQuery = async () => { + queryParams.pageNo = 1 + await getList() +} +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value?.resetFields() + handleQuery() +} +/** tab 切换 */ +const tabClick = async (tab: TabsPaneContext) => { + queryParams.status = tab.paneName + await getList() +} + +/** 处理退款 */ +const openAfterSaleDetail = (id: number) => { + push({ name: 'TradeAfterSaleDetail', params: { orderId: id } }) +} + +/** 查看订单详情 */ +const openOrderDetail = (id: number) => { + push({ name: 'TradeOrderDetail', params: { orderId: id } }) +} + +/** 商品图预览 */ +const imagePreview = (imgUrl: string) => { + createImageViewer({ + urlList: [imgUrl] + }) +} + +onMounted(async () => { + await getList() + // 设置 statuses 过滤 + for (const dict of getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)) { + statusTabs.value.push({ + label: dict.label, + value: dict.value + }) + } +}) +</script> diff --git a/src/views/mall/trade/order/detail/index.vue b/src/views/mall/trade/order/detail/index.vue index f43bccd2..ef2d9958 100644 --- a/src/views/mall/trade/order/detail/index.vue +++ b/src/views/mall/trade/order/detail/index.vue @@ -208,10 +208,10 @@ import * as TradeOrderApi from '@/api/mall/trade/order' import { floatToFixed2 } from '@/utils' import { DICT_TYPE } from '@/utils/dict' -import OrderUpdateRemarkForm from '@/views/mall/trade/order/components/OrderUpdateRemarkForm.vue' -import OrderDeliveryForm from '@/views/mall/trade/order/components/OrderDeliveryForm.vue' -import OrderUpdateAddressForm from '@/views/mall/trade/order/components/OrderUpdateAddressForm.vue' -import OrderUpdatePriceForm from '@/views/mall/trade/order/components/OrderUpdatePriceForm.vue' +import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue' +import OrderDeliveryForm from '@/views/mall/trade/order/form/OrderDeliveryForm.vue' +import OrderUpdateAddressForm from '@/views/mall/trade/order/form/OrderUpdateAddressForm.vue' +import OrderUpdatePriceForm from '@/views/mall/trade/order/form/OrderUpdatePriceForm.vue' defineOptions({ name: 'TradeOrderDetailForm' }) diff --git a/src/views/mall/trade/order/components/OrderDeliveryForm.vue b/src/views/mall/trade/order/form/OrderDeliveryForm.vue similarity index 94% rename from src/views/mall/trade/order/components/OrderDeliveryForm.vue rename to src/views/mall/trade/order/form/OrderDeliveryForm.vue index 6d4a07c4..14f9411d 100644 --- a/src/views/mall/trade/order/components/OrderDeliveryForm.vue +++ b/src/views/mall/trade/order/form/OrderDeliveryForm.vue @@ -34,7 +34,6 @@ import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express' import * as TradeOrderApi from '@/api/mall/trade/order' import { copyValueToTarget } from '@/utils' -// TODO @puhui999:要不 Form 保持和别的模块一样,和 index.vue 放在一个目录下,不作为 components;components 更多要支持给多个模块使用哈。 defineOptions({ name: 'OrderDeliveryForm' }) const { t } = useI18n() // 国际化 diff --git a/src/views/mall/trade/order/components/OrderUpdateAddressForm.vue b/src/views/mall/trade/order/form/OrderUpdateAddressForm.vue similarity index 100% rename from src/views/mall/trade/order/components/OrderUpdateAddressForm.vue rename to src/views/mall/trade/order/form/OrderUpdateAddressForm.vue diff --git a/src/views/mall/trade/order/components/OrderUpdatePriceForm.vue b/src/views/mall/trade/order/form/OrderUpdatePriceForm.vue similarity index 100% rename from src/views/mall/trade/order/components/OrderUpdatePriceForm.vue rename to src/views/mall/trade/order/form/OrderUpdatePriceForm.vue diff --git a/src/views/mall/trade/order/components/OrderUpdateRemarkForm.vue b/src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue similarity index 98% rename from src/views/mall/trade/order/components/OrderUpdateRemarkForm.vue rename to src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue index cbb7dbe9..bbc45a2c 100644 --- a/src/views/mall/trade/order/components/OrderUpdateRemarkForm.vue +++ b/src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue @@ -49,7 +49,6 @@ const submitForm = async () => { formLoading.value = true try { const data = unref(formData) - console.log(data) await TradeOrderApi.updateRemark(data) message.success(t('common.updateSuccess')) dialogVisible.value = false diff --git a/src/views/mall/trade/order/index.vue b/src/views/mall/trade/order/index.vue index 89200465..1f89e1de 100644 --- a/src/views/mall/trade/order/index.vue +++ b/src/views/mall/trade/order/index.vue @@ -64,10 +64,17 @@ /> </el-select> </el-form-item> - <!-- TODO @puhui999:要不加个 deliveryType 筛选;配送方式;然后如果选了快递,就有【快递公司】筛选;如果选了自提,就有【自提门店】;然后把他们这 3 个,坐在一个 el-form-item 里; - 目的是;有的时候,会筛选门店,然后做核销;这个时候,就需要筛选自提门店; - --> - <el-form-item label="快递公司" prop="type"> + <el-form-item label="配送方式" prop="deliveryType"> + <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="全部"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item v-if="queryParams.deliveryType === 1" label="快递公司"> <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部"> <el-option v-for="item in deliveryExpressList" @@ -77,7 +84,7 @@ /> </el-select> </el-form-item> - <el-form-item label="自提门店" prop="type"> + <el-form-item v-if="queryParams.deliveryType === 2" label="自提门店"> <el-select v-model="queryParams.pickUpStoreId" class="!w-280px" @@ -146,17 +153,17 @@ </template> <template #default="scope"> <el-table + :border="true" :data="scope.row.items" :header-cell-style="headerStyle" :span-method="spanMethod" - border style="width: 100%" > <el-table-column min-width="300" prop="spuName"> <template #header> <div class="flex items-center" - style="height: 35px; background-color: #f7f7f7; width: 100%" + style="width: 100%; height: 35px; background-color: #f7f7f7" > <span class="mr-20px">订单号:{{ scope.row.no }} </span> <span class="mr-20px">下单时间:{{ formatDate(scope.row.createTime) }}</span> @@ -173,8 +180,8 @@ :value="scope.row.payChannelCode" class="mr-20px" /> - <v-else class="mr-20px" v-else>未支付</v-else> - <span class="mr-20px" v-if="scope.row.payTime"> + <v-else v-else class="mr-20px">未支付</v-else> + <span v-if="scope.row.payTime" class="mr-20px"> 支付时间:{{ formatDate(scope.row.payTime) }} </span> <span>订单类型:</span> @@ -280,7 +287,8 @@ 发货 </el-dropdown-item> <el-dropdown-item command="remark"> - <Icon icon="ep:chat-line-square" /> 备注 + <Icon icon="ep:chat-line-square" /> + 备注 </el-dropdown-item> </el-dropdown-menu> </template> @@ -306,10 +314,10 @@ <OrderUpdateRemarkForm ref="updateRemarkForm" @success="getList" /> </template> -<script lang="ts" name="Order" setup> +<script lang="ts" setup> import type { FormInstance, TableColumnCtx } from 'element-plus' -import OrderDeliveryForm from './components/OrderDeliveryForm.vue' -import OrderUpdateRemarkForm from './components/OrderUpdateRemarkForm.vue' +import OrderDeliveryForm from '@/views/mall/trade/order/form/OrderDeliveryForm.vue' +import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue' import * as TradeOrderApi from '@/api/mall/trade/order' import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore' import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict' @@ -318,6 +326,8 @@ import { floatToFixed2 } from '@/utils' import { createImageViewer } from '@/components/ImageViewer' import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express' +defineOptions({ name: 'TradeOrder' }) + const { currentRoute, push } = useRouter() // 路由跳转 const loading = ref(true) // 列表的加载中 @@ -334,11 +344,13 @@ const queryParams = reactive({ userMobile: '', receiverName: '', receiverMobile: '', + terminal: '', type: null, status: null, payChannelCode: '', createTime: [], + deliveryType: null, spuName: '', itemCount: '', pickUpStoreId: [], @@ -384,7 +396,7 @@ const spanMethod = ({ row, rowIndex, columnIndex }: SpanMethodProps) => { (order) => order.items?.findIndex((item) => item.id === row.id) !== -1 )?.items?.length // 要合并的列,从零开始 - const colIndex = [3, 4, 5, 6] + const colIndex = [3, 4, 5, 6, 7] if (colIndex.includes(columnIndex)) { // 除了第一行其余的不要 if (rowIndex !== 0) { @@ -434,7 +446,7 @@ const imagePreview = (imgUrl: string) => { /** 查看订单详情 */ const openForm = (id: number) => { - push({ name: 'TradeOrderDetailForm', params: { orderId: id } }) + push({ name: 'TradeOrderDetail', params: { orderId: id } }) } /** 操作分发 */