feat: 积分商城-完成
This commit is contained in:
@@ -1,3 +1,10 @@
|
|||||||
# points-mall
|
# points-mall
|
||||||
|
|
||||||
积分商城-供应商模块-订单操作模块
|
积分商城-供应商模块-订单操作模块
|
||||||
|
|
||||||
|
## 页面与接口
|
||||||
|
|
||||||
|
- 商家处理台:`http://localhost:3001/#/joyful-selection/merchant`
|
||||||
|
- 供应商日志页面:`http://localhost:3001/#/joyful-selection/supplier-logs`
|
||||||
|
- 供应商日志接口:`GET http://localhost:8888/api/supplier-logs`
|
||||||
|
- 日志内容包含:当前后端接收请求、向zhgh发送请求、接收zhgh请求。
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import JoyfulSelectionAfterSaleSelect from '@/view/joyful-selection/after-sale/s
|
|||||||
import JoyfulSelectionAfterSaleReturn from '@/view/joyful-selection/after-sale/return/index.vue'
|
import JoyfulSelectionAfterSaleReturn from '@/view/joyful-selection/after-sale/return/index.vue'
|
||||||
import JoyfulSelectionAfterSaleExchange from '@/view/joyful-selection/after-sale/exchange/index.vue'
|
import JoyfulSelectionAfterSaleExchange from '@/view/joyful-selection/after-sale/exchange/index.vue'
|
||||||
import JoyfulSelectionMerchant from '@/view/joyful-selection/merchant/index.vue'
|
import JoyfulSelectionMerchant from '@/view/joyful-selection/merchant/index.vue'
|
||||||
|
import JoyfulSelectionMerchantAfterSaleDetail from '@/view/joyful-selection/merchant/after-sale-detail.vue'
|
||||||
|
import JoyfulSelectionSupplierLogs from '@/view/joyful-selection/supplier-logs/index.vue'
|
||||||
import JoyfulSelectionProfile from '@/view/joyful-selection/profile/index.vue'
|
import JoyfulSelectionProfile from '@/view/joyful-selection/profile/index.vue'
|
||||||
import JoyfulSelectionPoints from '@/view/joyful-selection/profile/points/index.vue'
|
import JoyfulSelectionPoints from '@/view/joyful-selection/profile/points/index.vue'
|
||||||
import HomeLiving from '@/view/home-living/index.vue'
|
import HomeLiving from '@/view/home-living/index.vue'
|
||||||
@@ -55,6 +57,8 @@ export default new Router({
|
|||||||
{ path: 'after-sale/return', component: JoyfulSelectionAfterSaleReturn },
|
{ path: 'after-sale/return', component: JoyfulSelectionAfterSaleReturn },
|
||||||
{ path: 'after-sale/exchange', component: JoyfulSelectionAfterSaleExchange },
|
{ path: 'after-sale/exchange', component: JoyfulSelectionAfterSaleExchange },
|
||||||
{ path: 'merchant', component: JoyfulSelectionMerchant },
|
{ path: 'merchant', component: JoyfulSelectionMerchant },
|
||||||
|
{ path: 'merchant/after-sale-detail', component: JoyfulSelectionMerchantAfterSaleDetail },
|
||||||
|
{ path: 'supplier-logs', component: JoyfulSelectionSupplierLogs },
|
||||||
{ path: 'profile', component: JoyfulSelectionProfile },
|
{ path: 'profile', component: JoyfulSelectionProfile },
|
||||||
{ path: 'points', component: JoyfulSelectionPoints }
|
{ path: 'points', component: JoyfulSelectionPoints }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
const UI_KEY = 'joyful-selection-ui'
|
||||||
|
|
||||||
|
const readUi = () => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(UI_KEY) || '{}')
|
||||||
|
} catch (error) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveUi = data => {
|
||||||
|
localStorage.setItem(UI_KEY, JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUiStore = defineStore('joyfulSelectionUi', {
|
||||||
|
state: () => ({
|
||||||
|
ordersTab: 0,
|
||||||
|
merchantTab: 'ship',
|
||||||
|
afterSaleTab: 'processing',
|
||||||
|
loaded: false
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
loadUi() {
|
||||||
|
const data = readUi()
|
||||||
|
this.ordersTab = Number(data.ordersTab || 0)
|
||||||
|
this.merchantTab = data.merchantTab || 'ship'
|
||||||
|
this.afterSaleTab = data.afterSaleTab || 'processing'
|
||||||
|
this.loaded = true
|
||||||
|
},
|
||||||
|
persistUi() {
|
||||||
|
saveUi({
|
||||||
|
ordersTab: this.ordersTab,
|
||||||
|
merchantTab: this.merchantTab,
|
||||||
|
afterSaleTab: this.afterSaleTab
|
||||||
|
})
|
||||||
|
this.loaded = true
|
||||||
|
},
|
||||||
|
setOrdersTab(value) {
|
||||||
|
this.ordersTab = Number(value || 0)
|
||||||
|
this.persistUi()
|
||||||
|
},
|
||||||
|
setMerchantTab(value) {
|
||||||
|
this.merchantTab = value || 'ship'
|
||||||
|
this.persistUi()
|
||||||
|
},
|
||||||
|
setAfterSaleTab(value) {
|
||||||
|
this.afterSaleTab = value || 'processing'
|
||||||
|
this.persistUi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="after-sale-detail-page">
|
<main class="after-sale-detail-page">
|
||||||
<van-nav-bar title="售后详情" left-arrow @click-left="$router.back()" />
|
<van-nav-bar title="售后详情" left-arrow @click-left="$router.replace('/joyful-selection/after-sale')" />
|
||||||
|
|
||||||
<section v-if="loading" class="skeleton-card">
|
<section v-if="loading" class="skeleton-card">
|
||||||
<van-skeleton title :row="3" />
|
<van-skeleton title :row="3" />
|
||||||
@@ -64,13 +64,14 @@
|
|||||||
|
|
||||||
<section v-show="!loading" class="detail-card info-card">
|
<section v-show="!loading" class="detail-card info-card">
|
||||||
<h3>售后信息</h3>
|
<h3>售后信息</h3>
|
||||||
<p><span>售后单号</span><b>{{ afterSale.serviceOrderId || '-' }}</b><button type="button" @click="copyServiceNo">复制</button></p>
|
<p class="service-no-row"><span>售后单号</span><b class="scrollable-no">{{ afterSale.serviceOrderId || '-' }}</b><button type="button" @click="copyServiceNo">复制</button></p>
|
||||||
|
<p><span>子订单号</span><b>{{ afterSale.orderNumberId || afterSale.subOrderId || '-' }}</b></p>
|
||||||
<p><span>售后类型</span><b>{{ afterSale.serviceTypeText || '-' }}</b></p>
|
<p><span>售后类型</span><b>{{ afterSale.serviceTypeText || '-' }}</b></p>
|
||||||
<p><span>售后原因</span><b>{{ afterSale.reason || '-' }}</b></p>
|
<p><span>售后原因</span><b>{{ afterSale.reason || '-' }}</b></p>
|
||||||
<p><span>问题描述</span><b>{{ afterSale.problemDescription || '-' }}</b></p>
|
<p><span>问题描述</span><b>{{ afterSale.problemDescription || '-' }}</b></p>
|
||||||
<p><span>申请时间</span><b>{{ afterSale.applyTime || '-' }}</b></p>
|
<p><span>申请时间</span><b>{{ afterSale.applyTime || '-' }}</b></p>
|
||||||
<p><span>取件方式</span><b>{{ pickupMethodText }}</b></p>
|
<p v-if="!isUnsignedAfterSale"><span>取件方式</span><b>{{ pickupMethodText }}</b></p>
|
||||||
<p><span>取件地址</span><b>{{ pickupAddressText }}</b></p>
|
<p v-if="!isUnsignedAfterSale"><span>取件地址</span><b>{{ pickupAddressText }}</b></p>
|
||||||
<div v-if="images.length" class="voucher-block">
|
<div v-if="images.length" class="voucher-block">
|
||||||
<h4>上传凭证</h4>
|
<h4>上传凭证</h4>
|
||||||
<div>
|
<div>
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
<footer v-show="!loading" class="bottom-actions">
|
<footer v-show="!loading" class="bottom-actions">
|
||||||
<button v-if="canCancel" type="button">撤销申请</button>
|
<button v-if="canCancel" type="button">撤销申请</button>
|
||||||
<button v-if="needUserShipNo" type="button" class="primary" @click="fillLogisticsNo">填写物流单号</button>
|
<button v-if="needUserShipNo" type="button" class="primary" @click="fillLogisticsNo">填写物流单号</button>
|
||||||
|
<button v-else-if="canConfirmExchangeReceive" type="button" class="primary" @click="confirmExchangeReceive">确认收货</button>
|
||||||
<button v-else type="button" class="primary" @click="contactService">联系客服</button>
|
<button v-else type="button" class="primary" @click="contactService">联系客服</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -162,6 +164,18 @@ export default {
|
|||||||
isExchange() {
|
isExchange() {
|
||||||
return Number(this.afterSale.serviceType) === SERVICE_TYPE.EXCHANGE
|
return Number(this.afterSale.serviceType) === SERVICE_TYPE.EXCHANGE
|
||||||
},
|
},
|
||||||
|
isUnsignedAfterSale() {
|
||||||
|
if (this.afterSale.isSigned === false || this.afterSale.isSigned === 0 || this.afterSale.isSigned === 'false') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.afterSale.isSigned === undefined &&
|
||||||
|
Number(this.afterSale.logisticsState) === LOGISTICS_STATE.NONE &&
|
||||||
|
!this.afterSale.pickupMethod &&
|
||||||
|
!this.afterSale.pickupName &&
|
||||||
|
!this.afterSale.pickupMobile &&
|
||||||
|
!this.afterSale.pickupAddress
|
||||||
|
},
|
||||||
statusCardClass() {
|
statusCardClass() {
|
||||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.REJECTED) return 'rejected'
|
if (Number(this.afterSale.serviceState) === SERVICE_STATE.REJECTED) return 'rejected'
|
||||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.APPROVED) return 'approved'
|
if (Number(this.afterSale.serviceState) === SERVICE_STATE.APPROVED) return 'approved'
|
||||||
@@ -178,8 +192,8 @@ export default {
|
|||||||
const logisticsState = Number(this.afterSale.logisticsState)
|
const logisticsState = Number(this.afterSale.logisticsState)
|
||||||
if (serviceState === SERVICE_STATE.REJECTED) return '审核未通过'
|
if (serviceState === SERVICE_STATE.REJECTED) return '审核未通过'
|
||||||
if (serviceState === SERVICE_STATE.FINISHED) return '售后完成'
|
if (serviceState === SERVICE_STATE.FINISHED) return '售后完成'
|
||||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已寄货'
|
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已发货'
|
||||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '商家待寄货'
|
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '商家待发货'
|
||||||
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '商品寄回中'
|
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '商品寄回中'
|
||||||
if (serviceState === SERVICE_STATE.APPROVED) return '审核通过'
|
if (serviceState === SERVICE_STATE.APPROVED) return '审核通过'
|
||||||
return '审核中'
|
return '审核中'
|
||||||
@@ -189,9 +203,10 @@ export default {
|
|||||||
const logisticsState = Number(this.afterSale.logisticsState)
|
const logisticsState = Number(this.afterSale.logisticsState)
|
||||||
if (serviceState === SERVICE_STATE.REJECTED) return '商家未通过您的售后申请,请查看审核意见'
|
if (serviceState === SERVICE_STATE.REJECTED) return '商家未通过您的售后申请,请查看审核意见'
|
||||||
if (serviceState === SERVICE_STATE.FINISHED) return '售后流程已完成'
|
if (serviceState === SERVICE_STATE.FINISHED) return '售后流程已完成'
|
||||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已寄出换货商品,请等待收货'
|
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已发出换货商品,请确认收货'
|
||||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '商家已收到商品,等待寄出换货商品'
|
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '商家已收到商品,等待发出换货商品'
|
||||||
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '您已寄回商品,等待商家收货'
|
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '您已寄回商品,等待商家收货'
|
||||||
|
if (this.isExchange && this.isUnsignedAfterSale && serviceState === SERVICE_STATE.APPROVED) return '商家已通过您的申请,等待商家发货'
|
||||||
if (serviceState === SERVICE_STATE.APPROVED) return '商家已通过您的申请,请按要求寄回商品'
|
if (serviceState === SERVICE_STATE.APPROVED) return '商家已通过您的申请,请按要求寄回商品'
|
||||||
return '商家正在审核您的售后申请,请耐心等待'
|
return '商家正在审核您的售后申请,请耐心等待'
|
||||||
},
|
},
|
||||||
@@ -229,14 +244,35 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (this.isUnsignedAfterSale && !this.isExchange) {
|
||||||
|
steps.splice(2)
|
||||||
|
steps.push({
|
||||||
|
key: 'finish',
|
||||||
|
title: '售后完成',
|
||||||
|
time: this.afterSale.finishTime,
|
||||||
|
desc: serviceState === SERVICE_STATE.FINISHED ? '商家同意,售后完成' : '商家同意后售后完成',
|
||||||
|
state: serviceState === SERVICE_STATE.FINISHED ? 'done' : serviceState >= SERVICE_STATE.APPROVED ? 'current' : 'todo'
|
||||||
|
})
|
||||||
|
|
||||||
|
return steps.map((step, index) => ({
|
||||||
|
...step,
|
||||||
|
state: serviceState === SERVICE_STATE.REJECTED && index > 1 ? 'todo' : step.state,
|
||||||
|
subTitle: step.state === 'done' ? '' : '-'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isExchange) {
|
if (this.isExchange) {
|
||||||
|
if (this.isUnsignedAfterSale) {
|
||||||
|
steps.splice(2)
|
||||||
|
}
|
||||||
|
|
||||||
steps.push(
|
steps.push(
|
||||||
{
|
{
|
||||||
key: 'merchantShip',
|
key: 'merchantShip',
|
||||||
title: '商家寄货',
|
title: '商家发货',
|
||||||
time: this.afterSale.merchantShipTime,
|
time: this.afterSale.merchantShipTime,
|
||||||
desc: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? '商家已寄出换货商品' : '等待商家寄出换货商品',
|
desc: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? '商家已发出换货商品' : '等待商家发出换货商品',
|
||||||
state: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? 'done' : logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED ? 'current' : 'todo'
|
state: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? 'done' : (this.isUnsignedAfterSale ? serviceState >= SERVICE_STATE.APPROVED : logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED) ? 'current' : 'todo'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'userReceive',
|
key: 'userReceive',
|
||||||
@@ -263,7 +299,7 @@ export default {
|
|||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
showReturnInfo() {
|
showReturnInfo() {
|
||||||
return Number(this.afterSale.serviceState) >= SERVICE_STATE.APPROVED
|
return Number(this.afterSale.serviceState) >= SERVICE_STATE.APPROVED && !this.isUnsignedAfterSale
|
||||||
},
|
},
|
||||||
returnInfoTitle() {
|
returnInfoTitle() {
|
||||||
return Number(this.afterSale.logisticsState) >= LOGISTICS_STATE.WAIT_USER_SHIP ? '退货地址' : '取件地址'
|
return Number(this.afterSale.logisticsState) >= LOGISTICS_STATE.WAIT_USER_SHIP ? '退货地址' : '取件地址'
|
||||||
@@ -274,6 +310,9 @@ export default {
|
|||||||
canCancel() {
|
canCancel() {
|
||||||
return Number(this.afterSale.serviceState) === SERVICE_STATE.PENDING_AUDIT
|
return Number(this.afterSale.serviceState) === SERVICE_STATE.PENDING_AUDIT
|
||||||
},
|
},
|
||||||
|
canConfirmExchangeReceive() {
|
||||||
|
return this.isExchange && Number(this.afterSale.logisticsState) === LOGISTICS_STATE.MERCHANT_SHIPPED
|
||||||
|
},
|
||||||
pickupMethodText() {
|
pickupMethodText() {
|
||||||
return this.afterSale.pickupMethod === 'mail' ? '自行寄回' : '上门取件'
|
return this.afterSale.pickupMethod === 'mail' ? '自行寄回' : '上门取件'
|
||||||
},
|
},
|
||||||
@@ -367,6 +406,26 @@ export default {
|
|||||||
this.submittingLogistics = false
|
this.submittingLogistics = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async confirmExchangeReceive() {
|
||||||
|
await Dialog.confirm({
|
||||||
|
title: '确认收到换货商品?',
|
||||||
|
message: '确认后该换货售后将完成。'
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
await postFunction('after-sale-logistics', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
serviceOrderId: this.afterSale.serviceOrderId,
|
||||||
|
logisticsState: LOGISTICS_STATE.USER_RECEIVED
|
||||||
|
})
|
||||||
|
Toast('已确认收货')
|
||||||
|
await this.loadDetail()
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '确认收货失败,请稍后再试')
|
||||||
|
}
|
||||||
|
},
|
||||||
contactService() {
|
contactService() {
|
||||||
Toast('客服功能开发中')
|
Toast('客服功能开发中')
|
||||||
}
|
}
|
||||||
@@ -643,6 +702,10 @@ export default {
|
|||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-card .service-no-row {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.address-card b,
|
.address-card b,
|
||||||
.info-card b {
|
.info-card b {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -653,15 +716,29 @@ export default {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-card .scrollable-no {
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: normal;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card .scrollable-no::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.info-card button {
|
.info-card button {
|
||||||
|
flex: none;
|
||||||
|
align-self: center;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
padding: 0 8px;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
|
||||||
color: #ff5b20;
|
color: #ff5b20;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: #fff0e9;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-card em {
|
.address-card em {
|
||||||
|
|||||||
@@ -21,36 +21,15 @@
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-show="!loading" class="exchange-card">
|
<section v-show="!loading" class="form-card">
|
||||||
<div class="exchange-head">
|
<div class="method-row">
|
||||||
<h3>换货商品</h3>
|
<span>是否签收</span>
|
||||||
<button type="button" @click="openExchangeGoodsPopup">{{ selectedExchangeGoods.sku ? '重新选择' : '选择商品' }}</button>
|
<button type="button" :class="{ active: isSigned === false }" @click="isSigned = false">未签收</button>
|
||||||
|
<button type="button" :class="{ active: isSigned === true }" @click="isSigned = true">已签收</button>
|
||||||
</div>
|
</div>
|
||||||
<article v-if="selectedExchangeGoods.sku" class="product-row">
|
|
||||||
<van-image class="product-thumb" :src="selectedExchangeGoods.image" fit="cover" />
|
|
||||||
<div class="product-info">
|
|
||||||
<h4>{{ selectedExchangeGoods.name || '-' }}</h4>
|
|
||||||
<p>{{ skuText(selectedExchangeGoods) }}</p>
|
|
||||||
<b>¥{{ selectedExchangeGoods.price || '0.00' }}</b>
|
|
||||||
</div>
|
|
||||||
<span>x1</span>
|
|
||||||
</article>
|
|
||||||
<p v-else>请选择与原商品价格一致的换货商品</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-show="!loading" class="form-card">
|
<section v-show="!loading && isSigned" class="form-card">
|
||||||
<div class="form-row reason-row" @click="showReasonPopup = true">
|
|
||||||
<span>售后原因</span>
|
|
||||||
<div class="reason-value">
|
|
||||||
<b>{{ selectedReasonText }}</b>
|
|
||||||
<van-icon name="arrow" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="desc-block">
|
|
||||||
<h3>问题描述</h3>
|
|
||||||
<textarea v-model="description" maxlength="200" placeholder="请详细描述遇到的问题(选填)"></textarea>
|
|
||||||
<em>{{ description.length }}/200</em>
|
|
||||||
</div>
|
|
||||||
<div class="method-row">
|
<div class="method-row">
|
||||||
<span>取件方式</span>
|
<span>取件方式</span>
|
||||||
<button type="button" :class="{ active: pickupMethod === 'door' }" @click="pickupMethod = 'door'">上门取件</button>
|
<button type="button" :class="{ active: pickupMethod === 'door' }" @click="pickupMethod = 'door'">上门取件</button>
|
||||||
@@ -64,6 +43,44 @@
|
|||||||
</div>
|
</div>
|
||||||
<van-icon name="arrow" />
|
<van-icon name="arrow" />
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="exchange-card">
|
||||||
|
<div class="exchange-head method-row">
|
||||||
|
<h3>换货商品</h3>
|
||||||
|
</div>
|
||||||
|
<div class="exchange-body">
|
||||||
|
<article v-if="selectedExchangeGoods.sku" class="product-row">
|
||||||
|
<van-image class="product-thumb" :src="selectedExchangeGoods.image" fit="cover" />
|
||||||
|
<div class="product-info">
|
||||||
|
<h4>{{ selectedExchangeGoods.name || '-' }}</h4>
|
||||||
|
<p>{{ skuText(selectedExchangeGoods) }}</p>
|
||||||
|
<b>¥{{ selectedExchangeGoods.price || '0.00' }}</b>
|
||||||
|
</div>
|
||||||
|
<span>x1</span>
|
||||||
|
</article>
|
||||||
|
<p v-else>请选择与原商品价格一致的换货商品</p>
|
||||||
|
<button type="button" @click="openExchangeGoodsPopup">{{ selectedExchangeGoods.sku ? '重新选择' : '选择商品' }}</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="form-card">
|
||||||
|
<div class="form-row reason-row" @click="showReasonPopup = true">
|
||||||
|
<span>售后原因</span>
|
||||||
|
<div class="reason-value">
|
||||||
|
<b>{{ selectedReasonText }}</b>
|
||||||
|
<van-icon name="arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="desc-block">
|
||||||
|
<h3>问题描述</h3>
|
||||||
|
<textarea v-model="description" maxlength="200" placeholder="请详细描述遇到的问题(选填)"></textarea>
|
||||||
|
<em>{{ description.length }}/200</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="upload-block">
|
<div class="upload-block">
|
||||||
<h3>上传图片(最多3张)</h3>
|
<h3>上传图片(最多3张)</h3>
|
||||||
<van-uploader
|
<van-uploader
|
||||||
@@ -99,7 +116,7 @@
|
|||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</section>
|
</section>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
<van-popup v-model="showExchangeGoodsPopup" round position="bottom">
|
<van-popup v-model="showExchangeGoodsPopup" round position="bottom" :style="{ height: '80vh' }">
|
||||||
<section class="exchange-popup">
|
<section class="exchange-popup">
|
||||||
<h3>选择换货商品</h3>
|
<h3>选择换货商品</h3>
|
||||||
<p>仅可选择价格为 ¥{{ originalProductPrice }} 的商品</p>
|
<p>仅可选择价格为 ¥{{ originalProductPrice }} 的商品</p>
|
||||||
@@ -130,6 +147,11 @@ const ORDER_EVENT_TYPE = {
|
|||||||
EXCHANGE_ORDER: 5
|
EXCHANGE_ORDER: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ORDER_STATE = {
|
||||||
|
RECEIVED: 4,
|
||||||
|
COMPLETED: 5
|
||||||
|
}
|
||||||
|
|
||||||
const SKU_COLOR_MAP = {
|
const SKU_COLOR_MAP = {
|
||||||
cream: '奶油白',
|
cream: '奶油白',
|
||||||
blue: '雾霾蓝',
|
blue: '雾霾蓝',
|
||||||
@@ -153,21 +175,22 @@ const REASON_OPTIONS = [
|
|||||||
export default {
|
export default {
|
||||||
name: 'JoyfulSelectionAfterSaleExchange',
|
name: 'JoyfulSelectionAfterSaleExchange',
|
||||||
data: () => ({
|
data: () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
currentOrder: {},
|
currentOrder: {},
|
||||||
currentSubOrder: {},
|
currentSubOrder: {},
|
||||||
pickupAddress: {},
|
pickupAddress: {},
|
||||||
description: '',
|
description: '',
|
||||||
pickupMethod: 'door',
|
pickupMethod: 'door',
|
||||||
showReasonPopup: false,
|
showReasonPopup: false,
|
||||||
showExchangeGoodsPopup: false,
|
showExchangeGoodsPopup: false,
|
||||||
selectedReason: '',
|
selectedReason: '',
|
||||||
exchangeGoods: [],
|
exchangeGoods: [],
|
||||||
selectedExchangeGoods: {},
|
selectedExchangeGoods: {},
|
||||||
fileList: [],
|
fileList: [],
|
||||||
uploadedImages: [],
|
uploadedImages: [],
|
||||||
submitting: false
|
submitting: false,
|
||||||
}),
|
isSigned: false
|
||||||
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||||
products() {
|
products() {
|
||||||
@@ -177,7 +200,9 @@ export default {
|
|||||||
return Number(this.products[0]?.price || this.currentSubOrder.amount || 0).toFixed(2)
|
return Number(this.products[0]?.price || this.currentSubOrder.amount || 0).toFixed(2)
|
||||||
},
|
},
|
||||||
samePriceGoods() {
|
samePriceGoods() {
|
||||||
return this.exchangeGoods.filter(item => Number(item.price || 0).toFixed(2) === this.originalProductPrice)
|
return this.exchangeGoods
|
||||||
|
.map(item => this.normalizeExchangeGoods(item))
|
||||||
|
.filter(item => Number(item.price || 0).toFixed(2) === this.originalProductPrice)
|
||||||
},
|
},
|
||||||
receiverNameText() {
|
receiverNameText() {
|
||||||
return this.pickupAddress.name || this.currentSubOrder.receiverName || this.currentOrder.receiverName || '-'
|
return this.pickupAddress.name || this.currentSubOrder.receiverName || this.currentOrder.receiverName || '-'
|
||||||
@@ -197,6 +222,12 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedReasonText() {
|
selectedReasonText() {
|
||||||
return this.reasonOptions.find(item => item.value === this.selectedReason)?.label || '请选择售后原因'
|
return this.reasonOptions.find(item => item.value === this.selectedReason)?.label || '请选择售后原因'
|
||||||
|
},
|
||||||
|
hasProductShipped() {
|
||||||
|
// Assuming currentSubOrder.logisticsState indicates if the product has been shipped to the user.
|
||||||
|
// If the product is not in 'NONE' (0) or 'WAIT_USER_SHIP' (1) state, it's considered shipped.
|
||||||
|
const logisticsState = Number(this.currentSubOrder.logisticsState);
|
||||||
|
return logisticsState !== 0 && logisticsState !== 1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -204,18 +235,35 @@ export default {
|
|||||||
this.loadPickupAddress()
|
this.loadPickupAddress()
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
|
this.loadOrderDetail()
|
||||||
this.loadPickupAddress()
|
this.loadPickupAddress()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||||
skuText(item = {}) {
|
skuText(item = {}) {
|
||||||
const sku = String(item.sku || item.skuText || '').trim()
|
if (item.skuText) {
|
||||||
|
return item.skuText
|
||||||
|
}
|
||||||
|
|
||||||
|
const sku = String(item.sku || '').trim()
|
||||||
const parts = sku.split('-').slice(-2).filter(Boolean)
|
const parts = sku.split('-').slice(-2).filter(Boolean)
|
||||||
const color = SKU_COLOR_MAP[parts[0]] || parts[0]
|
const color = SKU_COLOR_MAP[parts[0]] || parts[0]
|
||||||
const size = SKU_SIZE_MAP[parts[1]] || parts[1]
|
const size = SKU_SIZE_MAP[parts[1]] || parts[1]
|
||||||
|
|
||||||
return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
|
return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
|
||||||
},
|
},
|
||||||
|
normalizeExchangeGoods(item = {}) {
|
||||||
|
const color = 'cream'
|
||||||
|
const size = 'standard'
|
||||||
|
const sku = item.sku || `${item.id}-${color}-${size}`
|
||||||
|
const skuText = [SKU_COLOR_MAP[color] || color, SKU_SIZE_MAP[size] || size].filter(Boolean).join(' | ')
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
sku,
|
||||||
|
skuText
|
||||||
|
}
|
||||||
|
},
|
||||||
selectReason(item) {
|
selectReason(item) {
|
||||||
this.selectedReason = item.value
|
this.selectedReason = item.value
|
||||||
this.showReasonPopup = false
|
this.showReasonPopup = false
|
||||||
@@ -242,6 +290,20 @@ export default {
|
|||||||
this.selectedExchangeGoods = item
|
this.selectedExchangeGoods = item
|
||||||
this.showExchangeGoodsPopup = false
|
this.showExchangeGoodsPopup = false
|
||||||
},
|
},
|
||||||
|
inferIsSigned(order = {}, subOrder = {}) {
|
||||||
|
const state = Number(subOrder.orderState ?? order.orderState)
|
||||||
|
|
||||||
|
return [ORDER_STATE.RECEIVED, ORDER_STATE.COMPLETED].includes(state)
|
||||||
|
},
|
||||||
|
checkSignedMatch() {
|
||||||
|
const realSigned = this.inferIsSigned(this.currentOrder, this.currentSubOrder)
|
||||||
|
if (this.isSigned !== realSigned) {
|
||||||
|
Toast(realSigned ? '该订单已签收,请选择已签收' : '该订单未签收,请选择未签收')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
async afterRead(fileItems) {
|
async afterRead(fileItems) {
|
||||||
const items = Array.isArray(fileItems) ? fileItems : [fileItems]
|
const items = Array.isArray(fileItems) ? fileItems : [fileItems]
|
||||||
await Promise.all(items.map(async item => {
|
await Promise.all(items.map(async item => {
|
||||||
@@ -300,6 +362,7 @@ export default {
|
|||||||
|
|
||||||
this.currentOrder = data?.order || {}
|
this.currentOrder = data?.order || {}
|
||||||
this.currentSubOrder = data?.subOrder || {}
|
this.currentSubOrder = data?.subOrder || {}
|
||||||
|
this.isSigned = this.inferIsSigned(this.currentOrder, this.currentSubOrder)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast(error.message || '查询订单详情失败,请稍后再试')
|
Toast(error.message || '查询订单详情失败,请稍后再试')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -321,11 +384,30 @@ export default {
|
|||||||
Toast('订单信息不完整,请稍后再试')
|
Toast('订单信息不完整,请稍后再试')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!this.checkSignedMatch()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!this.selectedExchangeGoods.sku) {
|
if (!this.selectedExchangeGoods.sku) {
|
||||||
Toast('请选择换货商品')
|
Toast('请选择换货商品')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 售后原因必填
|
||||||
|
if (!this.selectedReason || this.selectedReason === '') {
|
||||||
|
Toast('请选择售后原因');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已签收,且取件方式是上门取件,则取件信息必填
|
||||||
|
if (this.isSigned && this.pickupMethod === 'door') {
|
||||||
|
if (!this.receiverNameText || this.receiverNameText === '-' ||
|
||||||
|
!this.receiverMobileText || this.receiverMobileText === '-' ||
|
||||||
|
!this.receiverAddressText || this.receiverAddressText === '-') {
|
||||||
|
Toast('请填写完整的取件信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const applied = await postFunction('after-sale-apply', {
|
const applied = await postFunction('after-sale-apply', {
|
||||||
supplierId: this.supplierId,
|
supplierId: this.supplierId,
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
@@ -337,17 +419,17 @@ export default {
|
|||||||
reason: this.selectedReasonText === '请选择售后原因' ? '换货' : this.selectedReasonText,
|
reason: this.selectedReasonText === '请选择售后原因' ? '换货' : this.selectedReasonText,
|
||||||
reasonType: this.selectedReason || 'OTHER',
|
reasonType: this.selectedReason || 'OTHER',
|
||||||
problemDescription: this.description,
|
problemDescription: this.description,
|
||||||
pickupMethod: this.pickupMethod,
|
pickupMethod: this.isSigned ? this.pickupMethod : undefined, // Conditionally set
|
||||||
pickupName: this.receiverNameText === '-' ? '' : this.receiverNameText,
|
pickupName: this.isSigned && this.receiverNameText !== '-' ? this.receiverNameText : undefined, // Conditionally set
|
||||||
pickupMobile: this.receiverMobileText === '-' ? '' : this.receiverMobileText,
|
pickupMobile: this.isSigned && this.receiverMobileText !== '-' ? this.receiverMobileText : undefined, // Conditionally set
|
||||||
pickupAddress: this.receiverAddressText === '-' ? '' : this.receiverAddressText,
|
pickupAddress: this.isSigned && this.receiverAddressText !== '-' ? this.receiverAddressText : undefined, // Conditionally set
|
||||||
exchangeProductSku: this.selectedExchangeGoods.sku,
|
exchangeProductSku: this.selectedExchangeGoods.sku,
|
||||||
exchangeProductName: this.selectedExchangeGoods.name,
|
exchangeProductName: this.selectedExchangeGoods.name,
|
||||||
exchangeProductPrice: this.selectedExchangeGoods.price,
|
exchangeProductPrice: this.selectedExchangeGoods.price,
|
||||||
images: this.uploadedImages
|
images: this.uploadedImages
|
||||||
})
|
})
|
||||||
|
|
||||||
Toast('换货申请已提交,等待商家审核')
|
Toast('敬请期待')
|
||||||
const serviceOrderId = applied?.serviceOrderId || applied?.afterSale?.serviceOrderId
|
const serviceOrderId = applied?.serviceOrderId || applied?.afterSale?.serviceOrderId
|
||||||
if (serviceOrderId) {
|
if (serviceOrderId) {
|
||||||
this.$router.replace({
|
this.$router.replace({
|
||||||
@@ -387,7 +469,7 @@ export default {
|
|||||||
.exchange-card,
|
.exchange-card,
|
||||||
.form-card {
|
.form-card {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 14px 12px;
|
padding: 0 12px 5px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@@ -405,32 +487,55 @@ export default {
|
|||||||
.exchange-head {
|
.exchange-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-head h3 {
|
.exchange-head h3 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-head button {
|
.exchange-body {
|
||||||
height: 30px;
|
display: flex;
|
||||||
padding: 0 14px;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-body button {
|
||||||
|
align-self: center;
|
||||||
|
flex: none;
|
||||||
|
min-width: 92px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 16px;
|
||||||
border: 1px solid #ff5b20;
|
border: 1px solid #ff5b20;
|
||||||
border-radius: 15px;
|
border-radius: 6px;
|
||||||
color: #ff5b20;
|
color: #ff5b20;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
background: #fff7f2;
|
background: #fff7f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-card > p {
|
.exchange-body > p {
|
||||||
margin: 12px 0 0;
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-card .product-row {
|
.exchange-card .product-row {
|
||||||
margin-top: 14px;
|
flex: 1;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-card .product-info {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-row {
|
.product-row {
|
||||||
@@ -483,7 +588,6 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
border-bottom: 1px solid #f2f2f2;
|
|
||||||
color: #222;
|
color: #222;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
@@ -740,9 +844,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.exchange-popup {
|
.exchange-popup {
|
||||||
max-height: 72vh;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 16px 12px 12px;
|
padding: 16px 12px 12px;
|
||||||
|
background: #f7f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-popup h3 {
|
.exchange-popup h3 {
|
||||||
@@ -754,6 +861,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.exchange-popup > p {
|
.exchange-popup > p {
|
||||||
|
flex: none;
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -761,59 +869,96 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.exchange-goods-list {
|
.exchange-goods-list {
|
||||||
max-height: calc(72vh - 68px);
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-goods-item {
|
.exchange-goods-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-height: 116px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 0;
|
gap: 12px;
|
||||||
border-bottom: 1px solid #f3f3f3;
|
padding: 10px 12px 10px 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 18px rgba(0, 0, 0, .045);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-item + .exchange-goods-item {
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-goods-thumb {
|
.exchange-goods-thumb {
|
||||||
width: 58px;
|
width: 88px;
|
||||||
height: 58px;
|
height: 88px;
|
||||||
|
min-width: 88px;
|
||||||
|
max-width: 88px;
|
||||||
|
min-height: 88px;
|
||||||
|
max-height: 88px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
flex: none;
|
flex: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 5px;
|
border-radius: 10px;
|
||||||
background: #f5f5f5;
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-thumb ::v-deep .van-image__img,
|
||||||
|
.exchange-goods-thumb ::v-deep img {
|
||||||
|
display: block;
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
border-radius: 10px;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-goods-item > div {
|
.exchange-goods-item > div {
|
||||||
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0 10px;
|
align-self: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-goods-item h4 {
|
.exchange-goods-item h4 {
|
||||||
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 0 5px;
|
margin: 0;
|
||||||
color: #222;
|
color: #111;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
line-height: 18px;
|
font-weight: 700;
|
||||||
|
line-height: 20px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-goods-item p {
|
.exchange-goods-item p {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 0 5px;
|
width: fit-content;
|
||||||
color: #999;
|
max-width: 100%;
|
||||||
|
margin: 6px 0 0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #8f8f8f;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
background: #f5f6f8;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-goods-item b {
|
.exchange-goods-item b {
|
||||||
color: #ff5b20;
|
color: #ff5b20;
|
||||||
font-size: 14px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-goods-item > .van-icon {
|
.exchange-goods-item > .van-icon {
|
||||||
flex: none;
|
flex: none;
|
||||||
color: #ff5b20;
|
color: #ff5b20;
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="after-sale-page">
|
<main class="after-sale-page">
|
||||||
<van-nav-bar title="退款/售后" left-arrow @click-left="$router.back()" />
|
<van-nav-bar title="退款/售后" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
|
||||||
|
|
||||||
<van-pull-refresh v-model="refreshing" class="after-sale-list" @refresh="onRefresh">
|
<van-pull-refresh v-model="refreshing" class="after-sale-list" @refresh="onRefresh">
|
||||||
<van-tabs v-model="activeTab" color="#ff5b20">
|
<van-tabs v-model="activeTab" color="#ff5b20">
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<span>{{ item.serviceTypeText }}</span>
|
<span>{{ item.serviceTypeText }}</span>
|
||||||
<b>{{ item.serviceStateText }}</b>
|
<b>{{ item.serviceStateText }}</b>
|
||||||
</header>
|
</header>
|
||||||
|
<span class="signed-stamp" :class="{ unsigned: item.isSigned === false }">{{ item.isSigned === false ? '未签收' : '已签收' }}</span>
|
||||||
|
|
||||||
<div class="after-sale-card__body">
|
<div class="after-sale-card__body">
|
||||||
<div
|
<div
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
|
|
||||||
<div class="after-sale-meta">
|
<div class="after-sale-meta">
|
||||||
<p><span>售后单号</span><b>{{ item.serviceOrderId }}</b></p>
|
<p><span>售后单号</span><b>{{ item.serviceOrderId }}</b></p>
|
||||||
|
<p><span>子订单号</span><b>{{ item.orderNumberId || item.subOrderId || '-' }}</b></p>
|
||||||
<p><span>申请时间</span><b>{{ item.applyTime || '-' }}</b></p>
|
<p><span>申请时间</span><b>{{ item.applyTime || '-' }}</b></p>
|
||||||
<p><span>物流状态</span><b>{{ item.logisticsStateText }}</b></p>
|
<p><span>物流状态</span><b>{{ item.logisticsStateText }}</b></p>
|
||||||
<p v-if="item.reason"><span>申请原因</span><b>{{ item.reason }}</b></p>
|
<p v-if="item.reason"><span>申请原因</span><b>{{ item.reason }}</b></p>
|
||||||
@@ -56,6 +58,7 @@ import { mapActions, mapState } from 'pinia'
|
|||||||
import { Toast } from 'vant'
|
import { Toast } from 'vant'
|
||||||
import { postFunction } from '@/api/supplied-service'
|
import { postFunction } from '@/api/supplied-service'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { useUiStore } from '@/store/ui'
|
||||||
|
|
||||||
const SKU_COLOR_MAP = {
|
const SKU_COLOR_MAP = {
|
||||||
cream: '奶油白',
|
cream: '奶油白',
|
||||||
@@ -71,13 +74,21 @@ const SKU_SIZE_MAP = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'JoyfulSelectionAfterSale',
|
name: 'JoyfulSelectionAfterSale',
|
||||||
data: () => ({
|
data: () => ({
|
||||||
activeTab: 'processing',
|
|
||||||
afterSaleItems: [],
|
afterSaleItems: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
refreshing: false
|
refreshing: false
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||||
|
...mapState(useUiStore, ['afterSaleTab']),
|
||||||
|
activeTab: {
|
||||||
|
get() {
|
||||||
|
return this.afterSaleTab
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setAfterSaleTab(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
currentAfterSaleItems() {
|
currentAfterSaleItems() {
|
||||||
return this.afterSaleItems.filter(item => {
|
return this.afterSaleItems.filter(item => {
|
||||||
const serviceState = Number(item.serviceState)
|
const serviceState = Number(item.serviceState)
|
||||||
@@ -91,10 +102,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.loadUi()
|
||||||
this.loadAfterSales()
|
this.loadAfterSales()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||||
|
...mapActions(useUiStore, ['loadUi', 'setAfterSaleTab']),
|
||||||
previewProducts(products = []) {
|
previewProducts(products = []) {
|
||||||
return products.slice(0, 3)
|
return products.slice(0, 3)
|
||||||
},
|
},
|
||||||
@@ -180,12 +193,48 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.after-sale-card {
|
.after-sale-card {
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signed-stamp {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
right: 15px;
|
||||||
|
bottom: 25px;
|
||||||
|
display: flex;
|
||||||
|
width: 95px;
|
||||||
|
height: 60px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid rgba(255, 91, 32, .8);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 18px;
|
||||||
|
opacity: .9;
|
||||||
|
background: rgba(255, 241, 232, .55);
|
||||||
|
transform: rotate(-18deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed-stamp::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 4px;
|
||||||
|
border: 1px dashed currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed-stamp.unsigned {
|
||||||
|
border-color: rgba(153, 153, 153, .9);
|
||||||
|
color: #666;
|
||||||
|
background: rgba(246, 247, 249, .7);
|
||||||
|
}
|
||||||
|
|
||||||
.skeleton-card {
|
.skeleton-card {
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -211,6 +260,15 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.after-sale-card__header span {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
background: #fff0e9;
|
||||||
|
}
|
||||||
|
|
||||||
.after-sale-card__body {
|
.after-sale-card__body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -20,20 +20,15 @@
|
|||||||
<span>x{{ item.number || 1 }}</span>
|
<span>x{{ item.number || 1 }}</span>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-show="!loading" class="form-card">
|
<section v-show="!loading" class="form-card">
|
||||||
<div class="form-row reason-row" @click="showReasonPopup = true">
|
<div class="method-row">
|
||||||
<span>售后原因</span>
|
<span>是否签收</span>
|
||||||
<div class="reason-value">
|
<button type="button" :class="{ active: isSigned === false }" @click="isSigned = false">未签收</button>
|
||||||
<b>{{ selectedReasonText }}</b>
|
<button type="button" :class="{ active: isSigned === true }" @click="isSigned = true">已签收</button>
|
||||||
<van-icon name="arrow" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="desc-block">
|
|
||||||
<h3>问题描述</h3>
|
|
||||||
<textarea v-model="description" maxlength="200" placeholder="请详细描述遇到的问题(选填)"></textarea>
|
|
||||||
<em>{{ description.length }}/200</em>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading && isSigned" class="form-card">
|
||||||
<div class="method-row">
|
<div class="method-row">
|
||||||
<span>取件方式</span>
|
<span>取件方式</span>
|
||||||
<button type="button" :class="{ active: pickupMethod === 'door' }" @click="pickupMethod = 'door'">上门取件</button>
|
<button type="button" :class="{ active: pickupMethod === 'door' }" @click="pickupMethod = 'door'">上门取件</button>
|
||||||
@@ -47,6 +42,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<van-icon name="arrow" />
|
<van-icon name="arrow" />
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="form-card">
|
||||||
|
<div class="form-row reason-row" @click="showReasonPopup = true">
|
||||||
|
<span>售后原因</span>
|
||||||
|
<div class="reason-value">
|
||||||
|
<b>{{ selectedReasonText }}</b>
|
||||||
|
<van-icon name="arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="desc-block">
|
||||||
|
<h3>问题描述</h3>
|
||||||
|
<textarea v-model="description" maxlength="200" placeholder="请详细描述遇到的问题(选填)"></textarea>
|
||||||
|
<em>{{ description.length }}/200</em>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="upload-block">
|
<div class="upload-block">
|
||||||
<h3>上传图片(最多3张)</h3>
|
<h3>上传图片(最多3张)</h3>
|
||||||
<van-uploader
|
<van-uploader
|
||||||
@@ -95,6 +109,11 @@ const ORDER_EVENT_TYPE = {
|
|||||||
RETURN_ORDER: 4
|
RETURN_ORDER: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ORDER_STATE = {
|
||||||
|
RECEIVED: 4,
|
||||||
|
COMPLETED: 5
|
||||||
|
}
|
||||||
|
|
||||||
const SKU_COLOR_MAP = {
|
const SKU_COLOR_MAP = {
|
||||||
cream: '奶油白',
|
cream: '奶油白',
|
||||||
blue: '雾霾蓝',
|
blue: '雾霾蓝',
|
||||||
@@ -125,18 +144,19 @@ const REASON_OPTIONS = [
|
|||||||
export default {
|
export default {
|
||||||
name: 'JoyfulSelectionAfterSaleReturn',
|
name: 'JoyfulSelectionAfterSaleReturn',
|
||||||
data: () => ({
|
data: () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
currentOrder: {},
|
currentOrder: {},
|
||||||
currentSubOrder: {},
|
currentSubOrder: {},
|
||||||
pickupAddress: {},
|
pickupAddress: {},
|
||||||
description: '',
|
description: '',
|
||||||
pickupMethod: 'door',
|
pickupMethod: 'door',
|
||||||
showReasonPopup: false,
|
showReasonPopup: false,
|
||||||
selectedReason: '',
|
selectedReason: '',
|
||||||
fileList: [],
|
fileList: [],
|
||||||
uploadedImages: [],
|
uploadedImages: [],
|
||||||
submitting: false
|
submitting: false,
|
||||||
}),
|
isSigned: false
|
||||||
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||||
products() {
|
products() {
|
||||||
@@ -160,6 +180,12 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedReasonText() {
|
selectedReasonText() {
|
||||||
return this.reasonOptions.find(item => item.value === this.selectedReason)?.label || '请选择售后原因'
|
return this.reasonOptions.find(item => item.value === this.selectedReason)?.label || '请选择售后原因'
|
||||||
|
},
|
||||||
|
hasProductShipped() {
|
||||||
|
// Assuming currentSubOrder.logisticsState indicates if the product has been shipped to the user.
|
||||||
|
// If the product is not in 'NONE' (0) or 'WAIT_USER_SHIP' (1) state, it's considered shipped.
|
||||||
|
const logisticsState = Number(this.currentSubOrder.logisticsState);
|
||||||
|
return logisticsState !== 0 && logisticsState !== 1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -167,6 +193,7 @@ export default {
|
|||||||
this.loadPickupAddress()
|
this.loadPickupAddress()
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
|
this.loadOrderDetail()
|
||||||
this.loadPickupAddress()
|
this.loadPickupAddress()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -183,6 +210,20 @@ export default {
|
|||||||
this.selectedReason = item.value
|
this.selectedReason = item.value
|
||||||
this.showReasonPopup = false
|
this.showReasonPopup = false
|
||||||
},
|
},
|
||||||
|
inferIsSigned(order = {}, subOrder = {}) {
|
||||||
|
const state = Number(subOrder.orderState ?? order.orderState)
|
||||||
|
|
||||||
|
return [ORDER_STATE.RECEIVED, ORDER_STATE.COMPLETED].includes(state)
|
||||||
|
},
|
||||||
|
checkSignedMatch() {
|
||||||
|
const realSigned = this.inferIsSigned(this.currentOrder, this.currentSubOrder)
|
||||||
|
if (this.isSigned !== realSigned) {
|
||||||
|
Toast(realSigned ? '该订单已签收,请选择已签收' : '该订单未签收,请选择未签收')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
async afterRead(fileItems) {
|
async afterRead(fileItems) {
|
||||||
const items = Array.isArray(fileItems) ? fileItems : [fileItems]
|
const items = Array.isArray(fileItems) ? fileItems : [fileItems]
|
||||||
await Promise.all(items.map(async item => {
|
await Promise.all(items.map(async item => {
|
||||||
@@ -241,6 +282,7 @@ export default {
|
|||||||
|
|
||||||
this.currentOrder = data?.order || {}
|
this.currentOrder = data?.order || {}
|
||||||
this.currentSubOrder = data?.subOrder || {}
|
this.currentSubOrder = data?.subOrder || {}
|
||||||
|
this.isSigned = this.inferIsSigned(this.currentOrder, this.currentSubOrder)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast(error.message || '查询订单详情失败,请稍后再试')
|
Toast(error.message || '查询订单详情失败,请稍后再试')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -262,6 +304,25 @@ export default {
|
|||||||
Toast('订单信息不完整,请稍后再试')
|
Toast('订单信息不完整,请稍后再试')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!this.checkSignedMatch()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 售后原因必填
|
||||||
|
if (!this.selectedReason || this.selectedReason === '') {
|
||||||
|
Toast('请选择售后原因');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已签收,且取件方式是上门取件,则取件信息必填
|
||||||
|
if (this.isSigned && this.pickupMethod === 'door') {
|
||||||
|
if (!this.receiverNameText || this.receiverNameText === '-' ||
|
||||||
|
!this.receiverMobileText || this.receiverMobileText === '-' ||
|
||||||
|
!this.receiverAddressText || this.receiverAddressText === '-') {
|
||||||
|
Toast('请填写完整的取件信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const applied = await postFunction('after-sale-apply', {
|
const applied = await postFunction('after-sale-apply', {
|
||||||
supplierId: this.supplierId,
|
supplierId: this.supplierId,
|
||||||
@@ -274,10 +335,10 @@ export default {
|
|||||||
reason: this.selectedReasonText === '请选择售后原因' ? '退货' : this.selectedReasonText,
|
reason: this.selectedReasonText === '请选择售后原因' ? '退货' : this.selectedReasonText,
|
||||||
reasonType: this.selectedReason || 'OTHER',
|
reasonType: this.selectedReason || 'OTHER',
|
||||||
problemDescription: this.description,
|
problemDescription: this.description,
|
||||||
pickupMethod: this.pickupMethod,
|
pickupMethod: this.isSigned ? this.pickupMethod : undefined, // Conditionally set
|
||||||
pickupName: this.receiverNameText === '-' ? '' : this.receiverNameText,
|
pickupName: this.isSigned && this.receiverNameText !== '-' ? this.receiverNameText : undefined, // Conditionally set
|
||||||
pickupMobile: this.receiverMobileText === '-' ? '' : this.receiverMobileText,
|
pickupMobile: this.isSigned && this.receiverMobileText !== '-' ? this.receiverMobileText : undefined, // Conditionally set
|
||||||
pickupAddress: this.receiverAddressText === '-' ? '' : this.receiverAddressText,
|
pickupAddress: this.isSigned && this.receiverAddressText !== '-' ? this.receiverAddressText : undefined, // Conditionally set
|
||||||
images: this.uploadedImages
|
images: this.uploadedImages
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -320,7 +381,7 @@ export default {
|
|||||||
.skeleton-card,
|
.skeleton-card,
|
||||||
.form-card {
|
.form-card {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 14px 12px;
|
padding: 0 12px 5px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@@ -384,7 +445,6 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
border-bottom: 1px solid #f2f2f2;
|
|
||||||
color: #222;
|
color: #222;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,767 @@
|
|||||||
|
<template>
|
||||||
|
<main class="after-sale-detail-page">
|
||||||
|
<van-nav-bar title="售后详情" left-arrow @click-left="$router.replace('/joyful-selection/merchant')" />
|
||||||
|
|
||||||
|
<section v-if="loading" class="skeleton-card">
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
<van-skeleton title :row="5" />
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="status-card" :class="statusCardClass">
|
||||||
|
<div class="status-icon">
|
||||||
|
<van-icon :name="statusIcon" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>{{ statusTitle }}</h2>
|
||||||
|
<p>{{ statusDesc }}</p>
|
||||||
|
<button v-if="afterSale.auditRemark" type="button" @click="showAuditRemark">查看审核意见</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="detail-card progress-card">
|
||||||
|
<h3>售后进度</h3>
|
||||||
|
<div class="timeline">
|
||||||
|
<div v-for="step in progressSteps" :key="step.key" class="timeline-item" :class="step.state">
|
||||||
|
<span class="timeline-dot">
|
||||||
|
<van-icon v-if="step.state === 'done'" name="success" />
|
||||||
|
</span>
|
||||||
|
<div class="timeline-main">
|
||||||
|
<b>{{ step.title }}</b>
|
||||||
|
<em>{{ step.time || step.subTitle }}</em>
|
||||||
|
</div>
|
||||||
|
<p>{{ step.desc }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="detail-card product-card">
|
||||||
|
<h3>售后商品</h3>
|
||||||
|
<article v-for="item in products" :key="item.id" class="product-row">
|
||||||
|
<van-image class="product-thumb" :src="item.image" fit="cover" />
|
||||||
|
<div class="product-info">
|
||||||
|
<h4>{{ item.name || '-' }}</h4>
|
||||||
|
<p>{{ skuText(item) }}</p>
|
||||||
|
<b>¥{{ item.price || afterSale.amount || '0.00' }}</b>
|
||||||
|
</div>
|
||||||
|
<span>x{{ item.number || 1 }}</span>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="!loading && showReturnInfo" class="detail-card address-card">
|
||||||
|
<h3>{{ returnInfoTitle }}</h3>
|
||||||
|
<p><span>联系人</span><b>{{ afterSale.pickupName || '-' }}</b></p>
|
||||||
|
<p><span>联系电话</span><b>{{ maskedMobile(afterSale.pickupMobile) }}</b></p>
|
||||||
|
<p><span>地址</span><b>{{ afterSale.pickupAddress || '-' }}</b></p>
|
||||||
|
<p v-if="afterSale.userLogisticsNo" class="logistics-row">
|
||||||
|
<span>寄回单号</span><b>{{ afterSale.userLogisticsCompany || '快递' }} {{ afterSale.userLogisticsNo }}</b>
|
||||||
|
</p>
|
||||||
|
<p v-if="afterSale.merchantLogisticsNo" class="logistics-row">
|
||||||
|
<span>换货单号</span><b>{{ afterSale.merchantLogisticsCompany || '快递' }} {{ afterSale.merchantLogisticsNo }}</b>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="detail-card info-card">
|
||||||
|
<h3>售后信息</h3>
|
||||||
|
<p class="service-no-row"><span>售后单号</span><b class="scrollable-no">{{ afterSale.serviceOrderId || '-' }}</b><button type="button" @click="copyServiceNo">复制</button></p>
|
||||||
|
<p><span>子订单号</span><b>{{ afterSale.orderNumberId || afterSale.subOrderId || '-' }}</b></p>
|
||||||
|
<p><span>售后类型</span><b>{{ afterSale.serviceTypeText || '-' }}</b></p>
|
||||||
|
<p><span>售后原因</span><b>{{ afterSale.reason || '-' }}</b></p>
|
||||||
|
<p><span>问题描述</span><b>{{ afterSale.problemDescription || '-' }}</b></p>
|
||||||
|
<p><span>申请时间</span><b>{{ afterSale.applyTime || '-' }}</b></p>
|
||||||
|
<p v-if="!isUnsignedAfterSale"><span>取件方式</span><b>{{ pickupMethodText }}</b></p>
|
||||||
|
<p v-if="!isUnsignedAfterSale"><span>取件地址</span><b>{{ pickupAddressText }}</b></p>
|
||||||
|
<div v-if="images.length" class="voucher-block">
|
||||||
|
<h4>上传凭证</h4>
|
||||||
|
<div>
|
||||||
|
<van-image v-for="image in images" :key="image.url" :src="image.url" fit="cover" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer v-show="!loading && merchantActions.length" class="bottom-actions">
|
||||||
|
<button
|
||||||
|
v-for="action in merchantActions"
|
||||||
|
:key="action.key"
|
||||||
|
type="button"
|
||||||
|
:class="{ primary: action.primary }"
|
||||||
|
@click="handleAction(action.key)"
|
||||||
|
>
|
||||||
|
{{ action.text }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<van-popup v-model="showExchangeShipPopup" round position="bottom">
|
||||||
|
<section class="action-popup">
|
||||||
|
<h3>填写换货发货信息</h3>
|
||||||
|
<van-field v-model="exchangeShipForm.logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
||||||
|
<van-field v-model="exchangeShipForm.logisticsNo" label="快递单号" placeholder="请输入快递单号" />
|
||||||
|
<button type="button" @click="submitExchangeShip">确认发货</button>
|
||||||
|
</section>
|
||||||
|
</van-popup>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapState } from 'pinia'
|
||||||
|
import { Dialog, Toast } from 'vant'
|
||||||
|
import { postFunction } from '@/api/supplied-service'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const SERVICE_TYPE = {
|
||||||
|
RETURN: 4,
|
||||||
|
EXCHANGE: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVICE_STATE = {
|
||||||
|
PENDING_AUDIT: 0,
|
||||||
|
APPROVED: 1,
|
||||||
|
REJECTED: 2,
|
||||||
|
USER_SHIPPED: 3,
|
||||||
|
RECEIVED: 4,
|
||||||
|
FINISHED: 5,
|
||||||
|
CANCELLED: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOGISTICS_STATE = {
|
||||||
|
NONE: 0,
|
||||||
|
WAIT_USER_SHIP: 1,
|
||||||
|
USER_SHIPPED: 2,
|
||||||
|
MERCHANT_RECEIVED: 3,
|
||||||
|
MERCHANT_SHIPPED: 4,
|
||||||
|
USER_RECEIVED: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_COLOR_MAP = {
|
||||||
|
cream: '奶油白',
|
||||||
|
blue: '雾霾蓝',
|
||||||
|
orange: '暖橙色'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_SIZE_MAP = {
|
||||||
|
standard: '标准款',
|
||||||
|
plus: '升级款'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'JoyfulSelectionMerchantAfterSaleDetail',
|
||||||
|
data: () => ({
|
||||||
|
afterSale: {},
|
||||||
|
loading: false,
|
||||||
|
showExchangeShipPopup: false,
|
||||||
|
exchangeShipForm: {
|
||||||
|
logisticsCompany: '',
|
||||||
|
logisticsNo: ''
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapState(useUserStore, ['supplierId']),
|
||||||
|
products() {
|
||||||
|
return this.afterSale.products || []
|
||||||
|
},
|
||||||
|
images() {
|
||||||
|
return Array.isArray(this.afterSale.images)
|
||||||
|
? this.afterSale.images.map(item => ({
|
||||||
|
url: item.url || item.previewUrl || item
|
||||||
|
})).filter(item => item.url)
|
||||||
|
: []
|
||||||
|
},
|
||||||
|
isExchange() {
|
||||||
|
return Number(this.afterSale.serviceType) === SERVICE_TYPE.EXCHANGE
|
||||||
|
},
|
||||||
|
isUnsignedAfterSale() {
|
||||||
|
if (this.afterSale.isSigned === false || this.afterSale.isSigned === 0 || this.afterSale.isSigned === 'false') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.afterSale.isSigned === undefined &&
|
||||||
|
Number(this.afterSale.logisticsState) === LOGISTICS_STATE.NONE &&
|
||||||
|
!this.afterSale.pickupMethod &&
|
||||||
|
!this.afterSale.pickupName &&
|
||||||
|
!this.afterSale.pickupMobile &&
|
||||||
|
!this.afterSale.pickupAddress
|
||||||
|
},
|
||||||
|
statusCardClass() {
|
||||||
|
if (Number(this.afterSale.serviceState) === SERVICE_STATE.REJECTED) return 'rejected'
|
||||||
|
if (Number(this.afterSale.serviceState) === SERVICE_STATE.APPROVED) return 'approved'
|
||||||
|
if (Number(this.afterSale.serviceState) === SERVICE_STATE.FINISHED) return 'finished'
|
||||||
|
return 'pending'
|
||||||
|
},
|
||||||
|
statusIcon() {
|
||||||
|
if (this.statusCardClass === 'approved' || this.statusCardClass === 'finished') return 'success'
|
||||||
|
if (this.statusCardClass === 'rejected') return 'cross'
|
||||||
|
return 'clock-o'
|
||||||
|
},
|
||||||
|
statusTitle() {
|
||||||
|
const serviceState = Number(this.afterSale.serviceState)
|
||||||
|
const logisticsState = Number(this.afterSale.logisticsState)
|
||||||
|
if (serviceState === SERVICE_STATE.REJECTED) return '审核未通过'
|
||||||
|
if (serviceState === SERVICE_STATE.FINISHED) return '售后完成'
|
||||||
|
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已发货'
|
||||||
|
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '待商家发货'
|
||||||
|
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '用户已寄回'
|
||||||
|
if (serviceState === SERVICE_STATE.APPROVED) return '审核通过'
|
||||||
|
return '待审核'
|
||||||
|
},
|
||||||
|
statusDesc() {
|
||||||
|
const serviceState = Number(this.afterSale.serviceState)
|
||||||
|
const logisticsState = Number(this.afterSale.logisticsState)
|
||||||
|
if (serviceState === SERVICE_STATE.REJECTED) return '商家已拒绝该售后申请'
|
||||||
|
if (serviceState === SERVICE_STATE.FINISHED) return '售后流程已完成'
|
||||||
|
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '换货商品已发出,等待用户收货'
|
||||||
|
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '已收到寄回商品,请发出换货商品'
|
||||||
|
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '用户已寄回商品,请确认收货'
|
||||||
|
if (this.isExchange && this.isUnsignedAfterSale && serviceState === SERVICE_STATE.APPROVED) return '未签收换货申请已通过,请发出换货商品'
|
||||||
|
if (serviceState === SERVICE_STATE.APPROVED) return '已通过售后申请,等待用户寄回商品'
|
||||||
|
return '请审核用户提交的售后申请'
|
||||||
|
},
|
||||||
|
progressSteps() {
|
||||||
|
const serviceState = Number(this.afterSale.serviceState)
|
||||||
|
const logisticsState = Number(this.afterSale.logisticsState)
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
key: 'apply',
|
||||||
|
title: '提交申请',
|
||||||
|
time: this.afterSale.applyTime,
|
||||||
|
desc: '用户已提交售后申请',
|
||||||
|
state: 'done'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'audit',
|
||||||
|
title: '商家审核',
|
||||||
|
time: this.afterSale.auditTime,
|
||||||
|
desc: serviceState === SERVICE_STATE.REJECTED ? '商家审核未通过' : serviceState >= SERVICE_STATE.APPROVED ? '商家已审核通过' : '待商家审核',
|
||||||
|
state: serviceState >= SERVICE_STATE.APPROVED || serviceState === SERVICE_STATE.REJECTED ? 'done' : 'current'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userShip',
|
||||||
|
title: '寄回商品',
|
||||||
|
time: this.afterSale.userShipTime,
|
||||||
|
desc: logisticsState >= LOGISTICS_STATE.USER_SHIPPED ? '用户已寄回商品' : '等待用户寄回商品',
|
||||||
|
state: logisticsState >= LOGISTICS_STATE.USER_SHIPPED ? 'done' : logisticsState === LOGISTICS_STATE.WAIT_USER_SHIP ? 'current' : 'todo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'merchantReceive',
|
||||||
|
title: '商家收货',
|
||||||
|
time: this.afterSale.merchantReceiveTime,
|
||||||
|
desc: logisticsState >= LOGISTICS_STATE.MERCHANT_RECEIVED ? '商家已收到商品' : '商家尚未收到商品',
|
||||||
|
state: logisticsState >= LOGISTICS_STATE.MERCHANT_RECEIVED ? 'done' : logisticsState === LOGISTICS_STATE.USER_SHIPPED ? 'current' : 'todo'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.isUnsignedAfterSale && !this.isExchange) {
|
||||||
|
steps.splice(2)
|
||||||
|
steps.push({
|
||||||
|
key: 'finish',
|
||||||
|
title: '售后完成',
|
||||||
|
time: this.afterSale.finishTime,
|
||||||
|
desc: serviceState === SERVICE_STATE.FINISHED ? '售后已完成' : '审核通过后售后完成',
|
||||||
|
state: serviceState === SERVICE_STATE.FINISHED ? 'done' : serviceState >= SERVICE_STATE.APPROVED ? 'current' : 'todo'
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.normalizeSteps(steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isExchange) {
|
||||||
|
if (this.isUnsignedAfterSale) {
|
||||||
|
steps.splice(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
steps.push(
|
||||||
|
{
|
||||||
|
key: 'merchantShip',
|
||||||
|
title: '商家发货',
|
||||||
|
time: this.afterSale.merchantShipTime,
|
||||||
|
desc: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? '商家已发出换货商品' : '等待商家发出换货商品',
|
||||||
|
state: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? 'done' : (this.isUnsignedAfterSale ? serviceState >= SERVICE_STATE.APPROVED : logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED) ? 'current' : 'todo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userReceive',
|
||||||
|
title: '用户收货',
|
||||||
|
time: this.afterSale.finishTime,
|
||||||
|
desc: logisticsState >= LOGISTICS_STATE.USER_RECEIVED ? '用户已收到换货商品' : '等待用户收货',
|
||||||
|
state: logisticsState >= LOGISTICS_STATE.USER_RECEIVED ? 'done' : logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED ? 'current' : 'todo'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
steps.push({
|
||||||
|
key: 'finish',
|
||||||
|
title: '售后完成',
|
||||||
|
time: this.afterSale.finishTime,
|
||||||
|
desc: serviceState === SERVICE_STATE.FINISHED ? '售后流程已完成' : '售后流程未完成',
|
||||||
|
state: serviceState === SERVICE_STATE.FINISHED ? 'done' : !this.isExchange && logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED ? 'current' : 'todo'
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.normalizeSteps(steps)
|
||||||
|
},
|
||||||
|
showReturnInfo() {
|
||||||
|
return Number(this.afterSale.serviceState) >= SERVICE_STATE.APPROVED && !this.isUnsignedAfterSale
|
||||||
|
},
|
||||||
|
returnInfoTitle() {
|
||||||
|
return Number(this.afterSale.logisticsState) >= LOGISTICS_STATE.WAIT_USER_SHIP ? '退货地址' : '取件地址'
|
||||||
|
},
|
||||||
|
pickupMethodText() {
|
||||||
|
return this.afterSale.pickupMethod === 'mail' ? '自行寄回' : '上门取件'
|
||||||
|
},
|
||||||
|
pickupAddressText() {
|
||||||
|
return this.afterSale.pickupAddress || '-'
|
||||||
|
},
|
||||||
|
merchantActions() {
|
||||||
|
const serviceState = Number(this.afterSale.serviceState)
|
||||||
|
if (serviceState === SERVICE_STATE.PENDING_AUDIT) {
|
||||||
|
return [
|
||||||
|
{ key: 'reject', text: '拒绝' },
|
||||||
|
{ key: 'approve', text: '通过', primary: true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(this.afterSale.serviceState) === SERVICE_STATE.USER_SHIPPED) {
|
||||||
|
return [{ key: 'receive', text: '确认收货', primary: true }]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canShipExchange) {
|
||||||
|
return [{ key: 'shipExchange', text: '换货发货', primary: true }]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
canShipExchange() {
|
||||||
|
return this.isExchange &&
|
||||||
|
(Number(this.afterSale.serviceState) === SERVICE_STATE.RECEIVED ||
|
||||||
|
(this.afterSale.isSigned === false && Number(this.afterSale.serviceState) === SERVICE_STATE.APPROVED))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadDetail()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||||
|
normalizeSteps(steps) {
|
||||||
|
const serviceState = Number(this.afterSale.serviceState)
|
||||||
|
return steps.map((step, index) => ({
|
||||||
|
...step,
|
||||||
|
state: serviceState === SERVICE_STATE.REJECTED && index > 1 ? 'todo' : step.state,
|
||||||
|
subTitle: step.state === 'done' ? '' : '-'
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
skuText(item = {}) {
|
||||||
|
const sku = String(item.sku || item.skuText || '').trim()
|
||||||
|
const parts = sku.split('-').slice(-2).filter(Boolean)
|
||||||
|
const color = SKU_COLOR_MAP[parts[0]] || parts[0]
|
||||||
|
const size = SKU_SIZE_MAP[parts[1]] || parts[1]
|
||||||
|
|
||||||
|
return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
|
||||||
|
},
|
||||||
|
maskedMobile(mobile = '') {
|
||||||
|
return String(mobile).replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2') || '-'
|
||||||
|
},
|
||||||
|
async loadDetail() {
|
||||||
|
try {
|
||||||
|
this.loading = true
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
const data = await postFunction('merchant-after-sale-detail', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
serviceOrderId: String(this.$route.query.serviceOrderId || '')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.afterSale = data?.afterSale || {}
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '查询售后详情失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyServiceNo() {
|
||||||
|
if (navigator.clipboard && this.afterSale.serviceOrderId) {
|
||||||
|
navigator.clipboard.writeText(this.afterSale.serviceOrderId)
|
||||||
|
}
|
||||||
|
Toast('已复制')
|
||||||
|
},
|
||||||
|
showAuditRemark() {
|
||||||
|
Dialog.alert({
|
||||||
|
title: '审核意见',
|
||||||
|
message: this.afterSale.auditRemark
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleAction(key) {
|
||||||
|
if (key === 'approve') {
|
||||||
|
this.auditAfterSale(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (key === 'reject') {
|
||||||
|
this.auditAfterSale(2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (key === 'receive') {
|
||||||
|
this.receiveAfterSale()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (key === 'shipExchange') {
|
||||||
|
this.openExchangeShipPopup()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async auditAfterSale(auditStatus) {
|
||||||
|
await Dialog.confirm({
|
||||||
|
title: auditStatus === 1 ? '确认通过售后申请?' : '确认拒绝售后申请?',
|
||||||
|
message: auditStatus === 1 ? '通过后用户可寄回商品。' : '拒绝后该售后申请将结束。'
|
||||||
|
})
|
||||||
|
await postFunction('merchant-after-sale-audit', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
serviceOrderId: this.afterSale.serviceOrderId,
|
||||||
|
auditStatus,
|
||||||
|
auditRemark: auditStatus === 1 ? '审核通过' : '商家拒绝'
|
||||||
|
})
|
||||||
|
Toast('处理成功')
|
||||||
|
await this.loadDetail()
|
||||||
|
},
|
||||||
|
async receiveAfterSale() {
|
||||||
|
await Dialog.confirm({
|
||||||
|
title: '确认已收到寄回商品?',
|
||||||
|
message: '确认后退货售后会进入完成,换货售后会进入待商家寄货。'
|
||||||
|
})
|
||||||
|
await postFunction('merchant-after-sale-receive', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
serviceOrderId: this.afterSale.serviceOrderId
|
||||||
|
})
|
||||||
|
Toast('已确认收货')
|
||||||
|
await this.loadDetail()
|
||||||
|
},
|
||||||
|
openExchangeShipPopup() {
|
||||||
|
this.exchangeShipForm = {
|
||||||
|
logisticsCompany: '',
|
||||||
|
logisticsNo: ''
|
||||||
|
}
|
||||||
|
this.showExchangeShipPopup = true
|
||||||
|
},
|
||||||
|
async submitExchangeShip() {
|
||||||
|
if (!this.exchangeShipForm.logisticsCompany || !this.exchangeShipForm.logisticsNo) {
|
||||||
|
Toast('请填写完整发货信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await postFunction('merchant-after-sale-ship', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
serviceOrderId: this.afterSale.serviceOrderId,
|
||||||
|
logisticsCompany: this.exchangeShipForm.logisticsCompany,
|
||||||
|
logisticsNo: this.exchangeShipForm.logisticsNo
|
||||||
|
})
|
||||||
|
Toast('换货发货成功')
|
||||||
|
this.showExchangeShipPopup = false
|
||||||
|
await this.loadDetail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.after-sale-detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 56px 10px 78px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-sale-detail-page ::v-deep .van-nav-bar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card,
|
||||||
|
.skeleton-card,
|
||||||
|
.detail-card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card {
|
||||||
|
padding: 18px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 18px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: none;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
background: #ff9f1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.approved .status-icon,
|
||||||
|
.status-card.finished .status-icon {
|
||||||
|
background: #20b879;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.rejected .status-icon {
|
||||||
|
background: #ee3f4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card h2 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card button {
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 0;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 12px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card button {
|
||||||
|
flex: none;
|
||||||
|
align-self: center;
|
||||||
|
height: 24px;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 12px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
padding: 14px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 6px;
|
||||||
|
width: 1px;
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item:last-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
background: #d8d8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item.done .timeline-dot {
|
||||||
|
background: #20b879;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item.current .timeline-dot {
|
||||||
|
background: #ff5b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-main b {
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-main em,
|
||||||
|
.timeline-item p {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-thumb {
|
||||||
|
flex: none;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h4 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info p {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info b {
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row > span {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-card p,
|
||||||
|
.info-card p {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-card b,
|
||||||
|
.info-card b {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card .service-no-row {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card .scrollable-no {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-break: normal;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card .scrollable-no::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voucher-block h4 {
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voucher-block div {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voucher-block .van-image {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 -4px 14px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions button,
|
||||||
|
.action-popup button {
|
||||||
|
min-width: 96px;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 18px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions button.primary,
|
||||||
|
.action-popup button {
|
||||||
|
border-color: #ff5b20;
|
||||||
|
color: #fff;
|
||||||
|
background: #ff5b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-popup {
|
||||||
|
padding: 18px 14px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-popup h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-popup button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<van-pull-refresh v-model="refreshing" class="merchant-refresh" @refresh="onRefresh">
|
<van-pull-refresh v-model="refreshing" class="merchant-refresh" @refresh="onRefresh">
|
||||||
<van-tabs v-model="activeTab" sticky offset-top="46" color="#ff5b20" @change="loadCurrentTab">
|
<van-tabs v-model="activeTab" sticky offset-top="46" color="#ff5b20" @change="loadCurrentTab">
|
||||||
<van-tab title="待发货" name="ship">
|
<van-tab title="待发货" name="ship" :badge="tabBadge('ship')">
|
||||||
<section class="list-panel">
|
<section class="list-panel">
|
||||||
<section v-if="loading" class="skeleton-card">
|
<section v-if="loading" class="skeleton-card">
|
||||||
<van-skeleton title :row="3" />
|
<van-skeleton title :row="3" />
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
||||||
<div class="product-info">
|
<div class="product-info">
|
||||||
<h4>{{ product.name || '-' }}</h4>
|
<h4>{{ product.name || '-' }}</h4>
|
||||||
<p>{{ product.sku || '-' }}</p>
|
<p>{{ skuText(product) }}</p>
|
||||||
<b>¥{{ product.price }}</b>
|
<b>¥{{ product.price }}</b>
|
||||||
</div>
|
</div>
|
||||||
<em>x{{ product.number || 1 }}</em>
|
<em>x{{ product.number || 1 }}</em>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
|
|
||||||
<van-tab title="售后审核" name="audit">
|
<van-tab title="售后审核" name="audit" :badge="tabBadge('audit')">
|
||||||
<section class="list-panel">
|
<section class="list-panel">
|
||||||
<section v-if="loading" class="skeleton-card">
|
<section v-if="loading" class="skeleton-card">
|
||||||
<van-skeleton title :row="3" />
|
<van-skeleton title :row="3" />
|
||||||
@@ -48,16 +48,17 @@
|
|||||||
</section>
|
</section>
|
||||||
<van-empty v-else-if="auditItems.length === 0" description="暂无待审核售后" />
|
<van-empty v-else-if="auditItems.length === 0" description="暂无待审核售后" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<article v-for="item in auditItems" :key="item.serviceOrderId" class="work-card">
|
<article v-for="item in auditItems" :key="item.serviceOrderId" class="work-card audit-card" @click="goAfterSaleDetail(item)">
|
||||||
<header>
|
<header>
|
||||||
<b>{{ item.serviceOrderId }}</b>
|
<b>{{ item.serviceOrderId }}</b>
|
||||||
<span>{{ item.serviceTypeText }}</span>
|
<span>{{ item.serviceTypeText }}</span>
|
||||||
</header>
|
</header>
|
||||||
|
<span class="signed-stamp" :class="{ unsigned: item.isSigned === false }">{{ item.isSigned === false ? '未签收' : '已签收' }}</span>
|
||||||
<div v-for="product in item.products" :key="product.id" class="product-row">
|
<div v-for="product in item.products" :key="product.id" class="product-row">
|
||||||
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
||||||
<div class="product-info">
|
<div class="product-info">
|
||||||
<h4>{{ product.name || '-' }}</h4>
|
<h4>{{ product.name || '-' }}</h4>
|
||||||
<p>{{ product.sku || '-' }}</p>
|
<p>{{ skuText(product) }}</p>
|
||||||
<b>¥{{ product.price }}</b>
|
<b>¥{{ product.price }}</b>
|
||||||
</div>
|
</div>
|
||||||
<em>x{{ product.number || 1 }}</em>
|
<em>x{{ product.number || 1 }}</em>
|
||||||
@@ -69,15 +70,16 @@
|
|||||||
<span>描述</span><b>{{ item.problemDescription || '-' }}</b>
|
<span>描述</span><b>{{ item.problemDescription || '-' }}</b>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<button type="button" @click="auditAfterSale(item, 2)">拒绝</button>
|
<button type="button" @click.stop="goAfterSaleDetail(item)">查看详情</button>
|
||||||
<button type="button" class="primary" @click="auditAfterSale(item, 1)">通过</button>
|
<button type="button" @click.stop="auditAfterSale(item, 2)">拒绝</button>
|
||||||
|
<button type="button" class="primary" @click.stop="auditAfterSale(item, 1)">通过</button>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
|
|
||||||
<van-tab title="售后处理" name="process">
|
<van-tab title="售后处理" name="process" :badge="tabBadge('process')">
|
||||||
<section class="list-panel">
|
<section class="list-panel">
|
||||||
<section v-if="loading" class="skeleton-card">
|
<section v-if="loading" class="skeleton-card">
|
||||||
<van-skeleton title :row="3" />
|
<van-skeleton title :row="3" />
|
||||||
@@ -85,16 +87,17 @@
|
|||||||
</section>
|
</section>
|
||||||
<van-empty v-else-if="processItems.length === 0" description="暂无处理中售后" />
|
<van-empty v-else-if="processItems.length === 0" description="暂无处理中售后" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<article v-for="item in processItems" :key="item.serviceOrderId" class="work-card">
|
<article v-for="item in processItems" :key="item.serviceOrderId" class="work-card" @click="goAfterSaleDetail(item)">
|
||||||
<header>
|
<header>
|
||||||
<b>{{ item.serviceOrderId }}</b>
|
<b>{{ item.serviceOrderId }}</b>
|
||||||
<span>{{ item.serviceStateText }}</span>
|
<span>{{ item.serviceStateText }}</span>
|
||||||
</header>
|
</header>
|
||||||
|
<span class="signed-stamp" :class="{ unsigned: item.isSigned === false }">{{ item.isSigned === false ? '未签收' : '已签收' }}</span>
|
||||||
<div v-for="product in item.products" :key="product.id" class="product-row">
|
<div v-for="product in item.products" :key="product.id" class="product-row">
|
||||||
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
||||||
<div class="product-info">
|
<div class="product-info">
|
||||||
<h4>{{ product.name || '-' }}</h4>
|
<h4>{{ product.name || '-' }}</h4>
|
||||||
<p>{{ product.sku || '-' }}</p>
|
<p>{{ skuText(product) }}</p>
|
||||||
<b>¥{{ product.price }}</b>
|
<b>¥{{ product.price }}</b>
|
||||||
</div>
|
</div>
|
||||||
<em>x{{ product.number || 1 }}</em>
|
<em>x{{ product.number || 1 }}</em>
|
||||||
@@ -106,8 +109,49 @@
|
|||||||
<span>取件地址</span><b>{{ item.pickupAddress || '-' }}</b>
|
<span>取件地址</span><b>{{ item.pickupAddress || '-' }}</b>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<button v-if="canReceiveAfterSale(item)" type="button" class="primary" @click="receiveAfterSale(item)">确认收货</button>
|
<button type="button" @click.stop="goAfterSaleDetail(item)">查看详情</button>
|
||||||
<button v-if="canShipExchange(item)" type="button" class="primary" @click="openExchangeShipPopup(item)">换货发货</button>
|
<button v-if="canReceiveAfterSale(item)" type="button" class="primary" @click.stop="receiveAfterSale(item)">确认收货</button>
|
||||||
|
<button v-if="canShipExchange(item)" type="button" class="primary" @click.stop="openExchangeShipPopup(item)">换货发货</button>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</van-tab>
|
||||||
|
|
||||||
|
<van-tab title="历史售后" name="history">
|
||||||
|
<section class="list-panel">
|
||||||
|
<section v-if="loading" class="skeleton-card">
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
</section>
|
||||||
|
<van-empty v-else-if="historyItems.length === 0" description="暂无历史售后" />
|
||||||
|
<template v-else>
|
||||||
|
<article v-for="item in historyItems" :key="item.serviceOrderId" class="work-card" @click="goAfterSaleDetail(item)">
|
||||||
|
<header>
|
||||||
|
<b>{{ item.serviceOrderId }}</b>
|
||||||
|
<span>{{ item.serviceStateText }}</span>
|
||||||
|
</header>
|
||||||
|
<span class="signed-stamp" :class="{ unsigned: item.isSigned === false }">{{ item.isSigned === false ? '未签收' : '已签收' }}</span>
|
||||||
|
<div v-for="product in item.products" :key="product.id" class="product-row">
|
||||||
|
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
||||||
|
<div class="product-info">
|
||||||
|
<h4>{{ product.name || '-' }}</h4>
|
||||||
|
<p>{{ skuText(product) }}</p>
|
||||||
|
<b>¥{{ product.price }}</b>
|
||||||
|
</div>
|
||||||
|
<em>x{{ product.number || 1 }}</em>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>类型</span><b>{{ item.serviceTypeText || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>原因</span><b>{{ item.reason || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>申请时间</span><b>{{ item.applyTime || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button type="button" class="primary" @click.stop="goAfterSaleDetail(item)">查看详情</button>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
@@ -141,6 +185,7 @@ import { mapActions, mapState } from 'pinia'
|
|||||||
import { Dialog, Toast } from 'vant'
|
import { Dialog, Toast } from 'vant'
|
||||||
import { postFunction } from '@/api/supplied-service'
|
import { postFunction } from '@/api/supplied-service'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { useUiStore } from '@/store/ui'
|
||||||
|
|
||||||
const ORDER_STATE = {
|
const ORDER_STATE = {
|
||||||
FINISH_PAY: 1
|
FINISH_PAY: 1
|
||||||
@@ -156,15 +201,26 @@ const SERVICE_STATE = {
|
|||||||
RECEIVED: 4
|
RECEIVED: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SKU_COLOR_MAP = {
|
||||||
|
cream: '奶油白',
|
||||||
|
blue: '雾霾蓝',
|
||||||
|
orange: '暖橙色'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_SIZE_MAP = {
|
||||||
|
standard: '标准款',
|
||||||
|
plus: '升级款'
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'JoyfulSelectionMerchant',
|
name: 'JoyfulSelectionMerchant',
|
||||||
data: () => ({
|
data: () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
activeTab: 'ship',
|
|
||||||
orderItems: [],
|
orderItems: [],
|
||||||
auditItems: [],
|
auditItems: [],
|
||||||
processItems: [],
|
processItems: [],
|
||||||
|
historyItems: [],
|
||||||
currentOrder: null,
|
currentOrder: null,
|
||||||
currentAfterSale: null,
|
currentAfterSale: null,
|
||||||
showShipPopup: false,
|
showShipPopup: false,
|
||||||
@@ -179,19 +235,61 @@ export default {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useUserStore, ['supplierId'])
|
...mapState(useUserStore, ['supplierId']),
|
||||||
|
...mapState(useUiStore, ['merchantTab']),
|
||||||
|
activeTab: {
|
||||||
|
get() {
|
||||||
|
return this.merchantTab
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setMerchantTab(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.prepareSupplier().then(() => this.loadCurrentTab())
|
this.loadUi()
|
||||||
|
this.prepareSupplier().then(() => this.loadAllTabs())
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useUserStore, ['fetchSupplierInfo', 'fetchBridgeToken']),
|
...mapActions(useUserStore, ['fetchSupplierInfo', 'fetchBridgeToken']),
|
||||||
|
...mapActions(useUiStore, ['loadUi', 'setMerchantTab']),
|
||||||
|
skuText(item = {}) {
|
||||||
|
const sku = String(item.sku || item.skuText || '').trim()
|
||||||
|
const parts = sku.split('-').slice(-2).filter(Boolean)
|
||||||
|
const color = SKU_COLOR_MAP[parts[0]] || parts[0]
|
||||||
|
const size = SKU_SIZE_MAP[parts[1]] || parts[1]
|
||||||
|
|
||||||
|
return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
|
||||||
|
},
|
||||||
|
tabBadge(name) {
|
||||||
|
const countMap = {
|
||||||
|
ship: this.orderItems.length,
|
||||||
|
audit: this.auditItems.length,
|
||||||
|
process: this.processItems.length
|
||||||
|
}
|
||||||
|
const count = countMap[name] || 0
|
||||||
|
|
||||||
|
return count > 0 ? count : ''
|
||||||
|
},
|
||||||
async prepareSupplier() {
|
async prepareSupplier() {
|
||||||
await this.fetchBridgeToken()
|
await this.fetchBridgeToken()
|
||||||
if (!this.supplierId) {
|
if (!this.supplierId) {
|
||||||
await this.fetchSupplierInfo()
|
await this.fetchSupplierInfo()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async loadAllTabs() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadOrders(),
|
||||||
|
this.loadAfterSales('audit'),
|
||||||
|
this.loadAfterSales('process'),
|
||||||
|
this.loadAfterSales('history')
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
async loadCurrentTab(showLoading = true) {
|
async loadCurrentTab(showLoading = true) {
|
||||||
const shouldShowLoading = typeof showLoading === 'boolean' ? showLoading : true
|
const shouldShowLoading = typeof showLoading === 'boolean' ? showLoading : true
|
||||||
if (shouldShowLoading) {
|
if (shouldShowLoading) {
|
||||||
@@ -209,12 +307,18 @@ export default {
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else if (this.activeTab === 'process') {
|
||||||
await this.loadAfterSales('process').finally(() => {
|
await this.loadAfterSales('process').finally(() => {
|
||||||
if (shouldShowLoading) {
|
if (shouldShowLoading) {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
await this.loadAfterSales('history').finally(() => {
|
||||||
|
if (shouldShowLoading) {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onRefresh() {
|
async onRefresh() {
|
||||||
@@ -238,10 +342,20 @@ export default {
|
|||||||
})
|
})
|
||||||
if (scene === 'audit') {
|
if (scene === 'audit') {
|
||||||
this.auditItems = data?.afterSaleItems || []
|
this.auditItems = data?.afterSaleItems || []
|
||||||
|
} else if (scene === 'history') {
|
||||||
|
this.historyItems = data?.afterSaleItems || []
|
||||||
} else {
|
} else {
|
||||||
this.processItems = data?.afterSaleItems || []
|
this.processItems = data?.afterSaleItems || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
goAfterSaleDetail(item) {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/joyful-selection/merchant/after-sale-detail',
|
||||||
|
query: {
|
||||||
|
serviceOrderId: item.serviceOrderId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
openShipPopup(item) {
|
openShipPopup(item) {
|
||||||
this.currentOrder = item
|
this.currentOrder = item
|
||||||
this.shipForm = {
|
this.shipForm = {
|
||||||
@@ -285,7 +399,9 @@ export default {
|
|||||||
return Number(item.serviceState) === SERVICE_STATE.USER_SHIPPED
|
return Number(item.serviceState) === SERVICE_STATE.USER_SHIPPED
|
||||||
},
|
},
|
||||||
canShipExchange(item) {
|
canShipExchange(item) {
|
||||||
return Number(item.serviceType) === SERVICE_TYPE.EXCHANGE && Number(item.serviceState) === SERVICE_STATE.RECEIVED
|
return Number(item.serviceType) === SERVICE_TYPE.EXCHANGE &&
|
||||||
|
(Number(item.serviceState) === SERVICE_STATE.RECEIVED ||
|
||||||
|
(item.isSigned === false && Number(item.serviceState) === SERVICE_STATE.APPROVED))
|
||||||
},
|
},
|
||||||
async receiveAfterSale(item) {
|
async receiveAfterSale(item) {
|
||||||
await Dialog.confirm({
|
await Dialog.confirm({
|
||||||
@@ -352,12 +468,52 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.work-card {
|
.work-card {
|
||||||
|
position: relative;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audit-card {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed-stamp {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 110px;
|
||||||
|
right: 45px;
|
||||||
|
display: flex;
|
||||||
|
width: 95px;
|
||||||
|
height: 60px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid rgba(255, 91, 32, .8);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 18px;
|
||||||
|
opacity: .9;
|
||||||
|
background: rgba(255, 241, 232, .55);
|
||||||
|
transform: rotate(-18deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed-stamp::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 4px;
|
||||||
|
border: 1px dashed currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed-stamp.unsigned {
|
||||||
|
border-color: rgba(153, 153, 153, .9);
|
||||||
|
color: #666;
|
||||||
|
background: rgba(246, 247, 249, .7);
|
||||||
|
}
|
||||||
|
|
||||||
.skeleton-card {
|
.skeleton-card {
|
||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -376,12 +532,18 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.work-card header b {
|
.work-card header b {
|
||||||
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
color: #222;
|
color: #222;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card header b::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.work-card header span {
|
.work-card header span {
|
||||||
@@ -393,6 +555,7 @@ export default {
|
|||||||
|
|
||||||
.product-row {
|
.product-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
@@ -473,19 +636,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.work-card footer button {
|
.work-card footer button {
|
||||||
min-width: 84px;
|
min-width: 88px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border: 1px solid #ddd;
|
padding: 0 16px;
|
||||||
border-radius: 17px;
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
background: #fff;
|
font-weight: 700;
|
||||||
|
background: #f6f7f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.work-card footer .primary {
|
.work-card footer .primary {
|
||||||
border-color: #ff5b20;
|
color: #ff5b20;
|
||||||
color: #fff;
|
background: #fff1e8;
|
||||||
background: #ff5b20;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-popup {
|
.action-popup {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<joyful-selection-payment-success-skeleton v-if="loading" />
|
<joyful-selection-payment-success-skeleton v-if="loading" />
|
||||||
<main v-else class="success-page">
|
<main v-else class="success-page">
|
||||||
<van-nav-bar title="支付成功" left-arrow @click-left="$router.back()" />
|
<van-nav-bar title="支付成功" left-arrow @click-left="goProductList" />
|
||||||
|
|
||||||
<section class="success-card">
|
<section class="success-card">
|
||||||
<div class="success-icon">
|
<div class="success-icon">
|
||||||
@@ -62,6 +62,9 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
goProductList() {
|
||||||
|
this.$router.replace('/joyful-selection/products')
|
||||||
|
},
|
||||||
formatTime(date) {
|
formatTime(date) {
|
||||||
const pad = value => String(value).padStart(2, '0')
|
const pad = value => String(value).padStart(2, '0')
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<joyful-selection-orders-skeleton v-if="loading" />
|
<joyful-selection-orders-skeleton v-if="loading" />
|
||||||
<main v-else class="orders-page">
|
<main v-else class="orders-page">
|
||||||
<van-nav-bar title="我的订单" left-arrow @click-left="$router.back()" />
|
<van-nav-bar title="我的订单" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
|
||||||
<van-tabs
|
<van-tabs
|
||||||
v-model="active"
|
v-model="active"
|
||||||
class="order-tabs"
|
class="order-tabs"
|
||||||
@@ -89,6 +89,7 @@ import { mapActions, mapState } from 'pinia'
|
|||||||
import { Dialog, Toast } from 'vant'
|
import { Dialog, Toast } from 'vant'
|
||||||
import { postFunction } from '@/api/supplied-service'
|
import { postFunction } from '@/api/supplied-service'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { useUiStore } from '@/store/ui'
|
||||||
import { savePaymentInfo } from '../../order/payment/payment-data'
|
import { savePaymentInfo } from '../../order/payment/payment-data'
|
||||||
import JoyfulSelectionOrdersSkeleton from '@/skeleton-view/joyful-selection/profile/orders/index.vue'
|
import JoyfulSelectionOrdersSkeleton from '@/skeleton-view/joyful-selection/profile/orders/index.vue'
|
||||||
|
|
||||||
@@ -136,7 +137,6 @@ export default {
|
|||||||
JoyfulSelectionOrdersSkeleton
|
JoyfulSelectionOrdersSkeleton
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
active: 0,
|
|
||||||
tabs: [
|
tabs: [
|
||||||
{ label: '全部', type: 'all' },
|
{ label: '全部', type: 'all' },
|
||||||
{ label: '待付款', type: 'pending-main' },
|
{ label: '待付款', type: 'pending-main' },
|
||||||
@@ -152,6 +152,15 @@ export default {
|
|||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||||
|
...mapState(useUiStore, ['ordersTab']),
|
||||||
|
active: {
|
||||||
|
get() {
|
||||||
|
return this.ordersTab
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setOrdersTab(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
activeTab() {
|
activeTab() {
|
||||||
return this.tabs[this.active] || this.tabs[0]
|
return this.tabs[this.active] || this.tabs[0]
|
||||||
},
|
},
|
||||||
@@ -189,7 +198,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||||
|
...mapActions(useUiStore, ['loadUi', 'setOrdersTab']),
|
||||||
applyRouteTab() {
|
applyRouteTab() {
|
||||||
|
this.loadUi()
|
||||||
const tabIndexMap = {
|
const tabIndexMap = {
|
||||||
'pending-pay': 1,
|
'pending-pay': 1,
|
||||||
'pending-ship': 2,
|
'pending-ship': 2,
|
||||||
@@ -199,7 +210,7 @@ export default {
|
|||||||
const nextActive = tabIndexMap[this.$route.query.tab]
|
const nextActive = tabIndexMap[this.$route.query.tab]
|
||||||
|
|
||||||
if (nextActive !== undefined) {
|
if (nextActive !== undefined) {
|
||||||
this.active = nextActive
|
this.setOrdersTab(nextActive)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
previewProducts(products = []) {
|
previewProducts(products = []) {
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<template>
|
||||||
|
<main class="supplier-logs-page">
|
||||||
|
<van-nav-bar title="供应商日志" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
|
<section class="toolbar">
|
||||||
|
<span :class="{ active: listening }">{{ listening ? '监听中' : '已暂停' }}</span>
|
||||||
|
<button type="button" @click="loadLogs">刷新</button>
|
||||||
|
<button type="button" @click="clearLogs">清除</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="console-panel">
|
||||||
|
<div v-if="consoleLogs.length === 0" class="console-empty">暂无日志,正在监听请求...</div>
|
||||||
|
<article v-for="item in consoleLogs" :key="item.id" class="console-line" :class="item.type">
|
||||||
|
<p><span>{{ formatTime(item.time) }}</span><b>{{ item.title }}:</b></p>
|
||||||
|
<pre>{{ formatRequestLine(item) }}</pre>
|
||||||
|
<pre>请求参数:{{ formatRequestParams(item.content) }}</pre>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Toast } from 'vant'
|
||||||
|
import { getFunction } from '@/api/supplied-service'
|
||||||
|
|
||||||
|
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/api'
|
||||||
|
const LOG_GROUPS = [
|
||||||
|
{ type: 'backend-receive', title: '当前后端接收请求', emptyText: '暂无后端接收请求' },
|
||||||
|
{ type: 'zhgh-send', title: '向ZHGH发送请求', emptyText: '暂无向ZHGH发送请求' },
|
||||||
|
{ type: 'zhgh-receive', title: '接收ZHGH请求', emptyText: '暂无接收ZHGH请求' }
|
||||||
|
]
|
||||||
|
const POLL_INTERVAL = 2000
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'JoyfulSelectionSupplierLogs',
|
||||||
|
data: () => ({
|
||||||
|
logs: [],
|
||||||
|
timer: null,
|
||||||
|
listening: false
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
consoleLogs() {
|
||||||
|
return this.logs
|
||||||
|
.filter(item => LOG_GROUPS.some(group => group.type === item.type))
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadLogs()
|
||||||
|
this.startListening()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopListening()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadLogs() {
|
||||||
|
try {
|
||||||
|
const data = await getFunction('supplier-logs')
|
||||||
|
this.logs = data?.logs || []
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '查询日志失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async clearLogs() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE.replace(/\/$/, '')}/supplier-logs`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
const payload = await response.json().catch(() => ({}))
|
||||||
|
if (!response.ok || payload.success === false) {
|
||||||
|
throw new Error(payload.error?.message || payload.msg || '清除日志失败')
|
||||||
|
}
|
||||||
|
this.logs = []
|
||||||
|
Toast('日志已清除')
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '清除日志失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startListening() {
|
||||||
|
this.stopListening()
|
||||||
|
this.listening = true
|
||||||
|
this.timer = window.setInterval(() => {
|
||||||
|
this.loadLogs()
|
||||||
|
}, POLL_INTERVAL)
|
||||||
|
},
|
||||||
|
stopListening() {
|
||||||
|
if (this.timer) {
|
||||||
|
window.clearInterval(this.timer)
|
||||||
|
this.timer = null
|
||||||
|
}
|
||||||
|
this.listening = false
|
||||||
|
},
|
||||||
|
formatTime(time) {
|
||||||
|
if (!time) return '-'
|
||||||
|
const date = new Date(time)
|
||||||
|
if (Number.isNaN(date.getTime())) return time
|
||||||
|
const pad = value => String(value).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
|
},
|
||||||
|
formatRequestLine(item) {
|
||||||
|
const content = item.content || {}
|
||||||
|
const method = content.method || 'POST'
|
||||||
|
const url = this.fullUrl(content.url || content.path || '-')
|
||||||
|
|
||||||
|
return `${method} ${url}`
|
||||||
|
},
|
||||||
|
fullUrl(url) {
|
||||||
|
if (!url || url === '-') return '-'
|
||||||
|
if (/^https?:\/\//i.test(url)) return url
|
||||||
|
const base = API_BASE.replace(/\/api\/?$/, '').replace(/\/$/, '')
|
||||||
|
const path = String(url).startsWith('/') ? url : `/${url}`
|
||||||
|
|
||||||
|
return `${base}${path}`
|
||||||
|
},
|
||||||
|
formatRequestParams(content = {}) {
|
||||||
|
const params = {
|
||||||
|
...(content.query || {}),
|
||||||
|
...(content.body || {})
|
||||||
|
}
|
||||||
|
const entries = Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.map(([key, value]) => `${key}=${this.formatParamValue(value)}`).join('&')
|
||||||
|
},
|
||||||
|
formatParamValue(value) {
|
||||||
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.supplier-logs-page {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 56px 10px 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplier-logs-page ::v-deep .van-nav-bar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar span {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 14px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar span::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #bbb;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar span.active {
|
||||||
|
color: #20b879;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar span.active::before {
|
||||||
|
background: #20b879;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar button {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid #ff5b20;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-panel {
|
||||||
|
height: calc(100vh - 116px);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-empty {
|
||||||
|
color: #7fda89;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line + .console-line {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line p {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
color: #7fda89;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line span {
|
||||||
|
color: #8aa0b4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line b {
|
||||||
|
color: #ffd166;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line.backend-receive b {
|
||||||
|
color: #7fda89;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line.zhgh-send b {
|
||||||
|
color: #5cc8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line.zhgh-receive b {
|
||||||
|
color: #ff8a5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line pre {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: #e6edf3;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -37,6 +37,13 @@ export const merchantAfterSaleListController = withErrorBoundary(async (request:
|
|||||||
return toHttpResponse(await merchantService.listAfterSales(await readJson<MerchantAfterSaleListInput>(request)))
|
return toHttpResponse(await merchantService.listAfterSales(await readJson<MerchantAfterSaleListInput>(request)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const merchantAfterSaleDetailController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
return toHttpResponse(await merchantService.detailAfterSale(await readJson<Pick<AfterSaleAuditInput, 'supplierId' | 'serviceOrderId'>>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
export const merchantAfterSaleAuditController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const merchantAfterSaleAuditController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
const methodResponse = guardMethod(request, ['POST'])
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
if (methodResponse) return methodResponse
|
if (methodResponse) return methodResponse
|
||||||
|
|||||||
@@ -236,6 +236,20 @@ export const orderDao = {
|
|||||||
return prisma.supplierAfterSale.count({ where })
|
return prisma.supplierAfterSale.count({ where })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
findProcessingAfterSaleOrderIds(supplierId: string, userId: string) {
|
||||||
|
return prisma.supplierAfterSale.findMany({
|
||||||
|
where: {
|
||||||
|
supplierId,
|
||||||
|
userId,
|
||||||
|
deleted: 0,
|
||||||
|
serviceState: { in: [0, 1, 3, 4] } // PENDING_AUDIT, APPROVED, USER_SHIPPED, RECEIVED
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
subOrderId: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
updatePushLog(id: number, data: Parameters<typeof prisma.supplierOrderPushLog.update>[0]['data']) {
|
updatePushLog(id: number, data: Parameters<typeof prisma.supplierOrderPushLog.update>[0]['data']) {
|
||||||
return prisma.supplierOrderPushLog.update({
|
return prisma.supplierOrderPushLog.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
} from './controllers/order.controller.js'
|
} from './controllers/order.controller.js'
|
||||||
import {
|
import {
|
||||||
merchantAfterSaleAuditController,
|
merchantAfterSaleAuditController,
|
||||||
|
merchantAfterSaleDetailController,
|
||||||
merchantAfterSaleListController,
|
merchantAfterSaleListController,
|
||||||
merchantAfterSaleReceiveController,
|
merchantAfterSaleReceiveController,
|
||||||
merchantAfterSaleShipController,
|
merchantAfterSaleShipController,
|
||||||
@@ -68,6 +69,7 @@ const handlers: Record<string, FunctionHandler> = {
|
|||||||
'cart-add': cartAddController,
|
'cart-add': cartAddController,
|
||||||
login: loginController,
|
login: loginController,
|
||||||
'merchant-after-sale-audit': merchantAfterSaleAuditController,
|
'merchant-after-sale-audit': merchantAfterSaleAuditController,
|
||||||
|
'merchant-after-sale-detail': merchantAfterSaleDetailController,
|
||||||
'merchant-after-sale-list': merchantAfterSaleListController,
|
'merchant-after-sale-list': merchantAfterSaleListController,
|
||||||
'merchant-after-sale-receive': merchantAfterSaleReceiveController,
|
'merchant-after-sale-receive': merchantAfterSaleReceiveController,
|
||||||
'merchant-after-sale-ship': merchantAfterSaleShipController,
|
'merchant-after-sale-ship': merchantAfterSaleShipController,
|
||||||
@@ -124,6 +126,16 @@ const writeResponse = async (ctx: Koa.Context, response: Response) => {
|
|||||||
|
|
||||||
const dispatch = (name: string) => async (ctx: Koa.Context) => {
|
const dispatch = (name: string) => async (ctx: Koa.Context) => {
|
||||||
logger.info('dispatch api request', { name, method: ctx.method, path: ctx.path })
|
logger.info('dispatch api request', { name, method: ctx.method, path: ctx.path })
|
||||||
|
logger.supplierDebug(ctx.path.startsWith('/supplierOpenApi/') ? 'zhgh-receive' : 'backend-receive',
|
||||||
|
ctx.path.startsWith('/supplierOpenApi/') ? '接收ZHGH请求' : '当前后端接收请求',
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
method: ctx.method,
|
||||||
|
path: ctx.path,
|
||||||
|
url: ctx.originalUrl,
|
||||||
|
query: ctx.query,
|
||||||
|
body: ctx.request.body || {}
|
||||||
|
})
|
||||||
const handler = handlers[name]
|
const handler = handlers[name]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
ctx.status = 404
|
ctx.status = 404
|
||||||
@@ -144,6 +156,23 @@ app.use(async (ctx, next) => {
|
|||||||
await next()
|
await next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.get('/api/supplier-logs', ctx => {
|
||||||
|
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
||||||
|
ctx.body = {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
logs: logger.getSupplierDebugLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
router.delete('/api/supplier-logs', ctx => {
|
||||||
|
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
||||||
|
logger.clearSupplierDebugLogs()
|
||||||
|
ctx.body = {
|
||||||
|
success: true,
|
||||||
|
data: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
router.all('/api/:name', async ctx => dispatch(ctx.params.name)(ctx))
|
router.all('/api/:name', async ctx => dispatch(ctx.params.name)(ctx))
|
||||||
router.post('/supplierOpenApi/pointOrder/queryOrderInfo', dispatch('supplier-order-info'))
|
router.post('/supplierOpenApi/pointOrder/queryOrderInfo', dispatch('supplier-order-info'))
|
||||||
router.post('/supplierOpenApi/debug/tokens', dispatch('supplier-debug-tokens'))
|
router.post('/supplierOpenApi/debug/tokens', dispatch('supplier-debug-tokens'))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from '../constants/order.js'
|
} from '../constants/order.js'
|
||||||
import { orderDao } from '../dao/order.dao.js'
|
import { orderDao } from '../dao/order.dao.js'
|
||||||
import { productDao } from '../dao/product.dao.js'
|
import { productDao } from '../dao/product.dao.js'
|
||||||
|
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||||
import { logger } from '../utils/logger.js'
|
import { logger } from '../utils/logger.js'
|
||||||
import { serviceFail, serviceOk } from '../types/service-result.js'
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export type AfterSaleApplyInput = {
|
|||||||
exchangeProductSku?: string
|
exchangeProductSku?: string
|
||||||
exchangeProductName?: string
|
exchangeProductName?: string
|
||||||
exchangeProductPrice?: number | string
|
exchangeProductPrice?: number | string
|
||||||
|
isSigned?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AfterSaleListInput = {
|
export type AfterSaleListInput = {
|
||||||
@@ -72,6 +74,12 @@ const supportedServiceTypes = new Set<number>([
|
|||||||
const afterSaleOrderStates = new Set<number>([
|
const afterSaleOrderStates = new Set<number>([
|
||||||
ORDER_STATE.FINISH_PAY,
|
ORDER_STATE.FINISH_PAY,
|
||||||
ORDER_STATE.IN_PROGRESS,
|
ORDER_STATE.IN_PROGRESS,
|
||||||
|
ORDER_STATE.RECEIVED,
|
||||||
|
ORDER_STATE.COMPLETED
|
||||||
|
])
|
||||||
|
|
||||||
|
const signedOrderStates = new Set<number>([
|
||||||
|
ORDER_STATE.RECEIVED,
|
||||||
ORDER_STATE.COMPLETED
|
ORDER_STATE.COMPLETED
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -128,6 +136,71 @@ const createServiceOrderId = (subOrderId: string, serviceType: number) => {
|
|||||||
return `AS${serviceType}_${subOrderId}_${Date.now()}`
|
return `AS${serviceType}_${subOrderId}_${Date.now()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOrderSigned = (orderState?: number | null) => signedOrderStates.has(Number(orderState))
|
||||||
|
|
||||||
|
const pushReturnOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>) => {
|
||||||
|
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
||||||
|
return serviceFail('售后订单信息不完整,无法推送退货单')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response, payload } = await bridgeFetchWithAuth<{ resultCode?: number; message?: string }>(record.supplierId, '/pointOrder/pushOrderInfoMsg', {
|
||||||
|
supplierId: record.supplierId,
|
||||||
|
orderType: ORDER_EVENT_TYPE.RETURN_ORDER,
|
||||||
|
orderId: record.serviceOrderId,
|
||||||
|
mainOrderId: record.subOrderId,
|
||||||
|
userId: record.userId,
|
||||||
|
time: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok || payload.resultCode !== 0) {
|
||||||
|
return serviceFail(payload.message || '退货单推送失败', 502, 'BRIDGE_RETURN_ORDER_FAILED')
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceOk({ serviceOrderId: record.serviceOrderId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushExchangeOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>) => {
|
||||||
|
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
||||||
|
return serviceFail('售后订单信息不完整,无法推送换货单')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response, payload } = await bridgeFetchWithAuth<{ resultCode?: number; message?: string }>(record.supplierId, '/pointOrder/pushOrderInfoMsg', {
|
||||||
|
supplierId: record.supplierId,
|
||||||
|
orderType: ORDER_EVENT_TYPE.EXCHANGE_ORDER,
|
||||||
|
orderId: record.serviceOrderId,
|
||||||
|
mainOrderId: record.subOrderId,
|
||||||
|
userId: record.userId,
|
||||||
|
time: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok || payload.resultCode !== 0) {
|
||||||
|
return serviceFail(payload.message || '换货单推送失败', 502, 'BRIDGE_EXCHANGE_ORDER_FAILED')
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceOk({ serviceOrderId: record.serviceOrderId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushCancelSubOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>) => {
|
||||||
|
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
||||||
|
return serviceFail('售后订单信息不完整,无法取消子单')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response, payload } = await bridgeFetchWithAuth<{ resultCode?: number; message?: string }>(record.supplierId, '/pointOrder/pushOrderInfoMsg', {
|
||||||
|
supplierId: record.supplierId,
|
||||||
|
orderType: ORDER_EVENT_TYPE.CANCEL_SUB,
|
||||||
|
orderId: record.subOrderId,
|
||||||
|
mainOrderId: record.mainOrderId,
|
||||||
|
userId: record.userId,
|
||||||
|
time: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok || payload.resultCode !== 0) {
|
||||||
|
return serviceFail(payload.message || '取消子单推送失败', 502, 'BRIDGE_CANCEL_SUB_FAILED')
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceOk({ serviceOrderId: record.serviceOrderId })
|
||||||
|
}
|
||||||
|
|
||||||
const afterSaleDetailPayload = (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>, subOrder?: Awaited<ReturnType<typeof orderDao.findOpenApiSubOrders>>[number] | null) => {
|
const afterSaleDetailPayload = (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>, subOrder?: Awaited<ReturnType<typeof orderDao.findOpenApiSubOrders>>[number] | null) => {
|
||||||
const requestBody = record.requestBody && typeof record.requestBody === 'object' ? record.requestBody as AfterSaleApplyInput : {}
|
const requestBody = record.requestBody && typeof record.requestBody === 'object' ? record.requestBody as AfterSaleApplyInput : {}
|
||||||
const products = subOrder?.products.map(product => ({
|
const products = subOrder?.products.map(product => ({
|
||||||
@@ -168,6 +241,7 @@ const afterSaleDetailPayload = (record: NonNullable<Awaited<ReturnType<typeof or
|
|||||||
price: requestBody.exchangeProductPrice || ''
|
price: requestBody.exchangeProductPrice || ''
|
||||||
} : null,
|
} : null,
|
||||||
images: Array.isArray(record.images) ? record.images : [],
|
images: Array.isArray(record.images) ? record.images : [],
|
||||||
|
isSigned: Boolean(record.pickupMethod || record.pickupName || record.pickupMobile || record.pickupAddress) || isOrderSigned(subOrder?.orderState),
|
||||||
auditRemark: record.auditRemark || '',
|
auditRemark: record.auditRemark || '',
|
||||||
userLogisticsNo: record.userLogisticsNo || '',
|
userLogisticsNo: record.userLogisticsNo || '',
|
||||||
userLogisticsCompany: record.userLogisticsCompany || '',
|
userLogisticsCompany: record.userLogisticsCompany || '',
|
||||||
@@ -247,6 +321,7 @@ export const afterSaleService = {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const amount = Number(input.amount || subOrder.totalPrice || 0)
|
const amount = Number(input.amount || subOrder.totalPrice || 0)
|
||||||
const serviceOrderId = createServiceOrderId(subOrderId, serviceType)
|
const serviceOrderId = createServiceOrderId(subOrderId, serviceType)
|
||||||
|
const { isSigned: _isSigned, ...requestBody } = input
|
||||||
|
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
await tx.supplierAfterSale.create({
|
await tx.supplierAfterSale.create({
|
||||||
@@ -270,7 +345,7 @@ export const afterSaleService = {
|
|||||||
pickupAddress: pickupAddress || null,
|
pickupAddress: pickupAddress || null,
|
||||||
images,
|
images,
|
||||||
applyTime: now,
|
applyTime: now,
|
||||||
requestBody: input
|
requestBody
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await tx.supplierOrderOperateLog.create({
|
await tx.supplierOrderOperateLog.create({
|
||||||
@@ -370,6 +445,30 @@ export const afterSaleService = {
|
|||||||
return serviceOk({ afterSale: afterSaleDetailPayload(record, subOrders[0]) })
|
return serviceOk({ afterSale: afterSaleDetailPayload(record, subOrders[0]) })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async merchantDetail(input: Pick<AfterSaleDetailInput, 'supplierId' | 'serviceOrderId'>) {
|
||||||
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
|
const serviceOrderId = String(input.serviceOrderId || '').trim()
|
||||||
|
logger.info('merchant after sale detail query', { supplierId, serviceOrderId })
|
||||||
|
|
||||||
|
if (!supplierId || !serviceOrderId) {
|
||||||
|
return serviceFail('供应商或售后单不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = await orderDao.findAfterSale({
|
||||||
|
supplierId,
|
||||||
|
serviceOrderId,
|
||||||
|
deleted: 0
|
||||||
|
})
|
||||||
|
if (!record) {
|
||||||
|
return serviceFail('售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
||||||
|
}
|
||||||
|
|
||||||
|
const subOrders = record.subOrderId
|
||||||
|
? await orderDao.findOpenApiSubOrders(supplierId, [record.subOrderId])
|
||||||
|
: []
|
||||||
|
return serviceOk({ afterSale: afterSaleDetailPayload(record, subOrders[0]) })
|
||||||
|
},
|
||||||
|
|
||||||
async audit(input: AfterSaleAuditInput) {
|
async audit(input: AfterSaleAuditInput) {
|
||||||
const supplierId = String(input.supplierId || '').trim()
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
const serviceOrderId = String(input.serviceOrderId || '').trim()
|
const serviceOrderId = String(input.serviceOrderId || '').trim()
|
||||||
@@ -385,7 +484,27 @@ export const afterSaleService = {
|
|||||||
return serviceFail('退款/售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
return serviceFail('退款/售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subOrders = record.subOrderId
|
||||||
|
? await orderDao.findOpenApiSubOrders(supplierId, [record.subOrderId])
|
||||||
|
: []
|
||||||
|
const isSigned = isOrderSigned(subOrders[0]?.orderState)
|
||||||
const approved = auditStatus === 1
|
const approved = auditStatus === 1
|
||||||
|
const isExchange = record.serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER
|
||||||
|
const finishUnsignedReturn = approved && !isSigned && record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER
|
||||||
|
const nextServiceState = approved
|
||||||
|
? (isSigned || isExchange ? AFTER_SALE_SERVICE_STATE.APPROVED : AFTER_SALE_SERVICE_STATE.FINISHED)
|
||||||
|
: AFTER_SALE_SERVICE_STATE.REJECTED
|
||||||
|
const nextLogisticsState = approved
|
||||||
|
? (isSigned ? AFTER_SALE_LOGISTICS_STATE.WAIT_USER_SHIP : AFTER_SALE_LOGISTICS_STATE.NONE)
|
||||||
|
: AFTER_SALE_LOGISTICS_STATE.NONE
|
||||||
|
|
||||||
|
if (finishUnsignedReturn) {
|
||||||
|
const cancelSubOrder = await pushCancelSubOrder(record)
|
||||||
|
if (!cancelSubOrder.success) {
|
||||||
|
return cancelSubOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
await tx.supplierAfterSale.update({
|
await tx.supplierAfterSale.update({
|
||||||
@@ -394,13 +513,27 @@ export const afterSaleService = {
|
|||||||
auditStatus,
|
auditStatus,
|
||||||
auditRemark: auditRemark || null,
|
auditRemark: auditRemark || null,
|
||||||
auditTime: now,
|
auditTime: now,
|
||||||
serviceState: approved ? AFTER_SALE_SERVICE_STATE.APPROVED : AFTER_SALE_SERVICE_STATE.REJECTED,
|
serviceState: nextServiceState,
|
||||||
logisticsState: approved ? AFTER_SALE_LOGISTICS_STATE.WAIT_USER_SHIP : AFTER_SALE_LOGISTICS_STATE.NONE
|
logisticsState: nextLogisticsState
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (finishUnsignedReturn && record.subOrderId) {
|
||||||
|
await tx.supplierOrderSub.updateMany({
|
||||||
|
where: {
|
||||||
|
supplierId,
|
||||||
|
orderId: record.subOrderId,
|
||||||
|
deleted: 0
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
orderState: ORDER_STATE.REFUNDED,
|
||||||
|
orderFinishTime: now,
|
||||||
|
updateTime: now
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return serviceOk({ serviceOrderId, auditStatus, serviceState: approved ? AFTER_SALE_SERVICE_STATE.APPROVED : AFTER_SALE_SERVICE_STATE.REJECTED })
|
return serviceOk({ serviceOrderId, auditStatus, serviceState: nextServiceState })
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateLogistics(input: AfterSaleLogisticsInput) {
|
async updateLogistics(input: AfterSaleLogisticsInput) {
|
||||||
@@ -417,9 +550,24 @@ export const afterSaleService = {
|
|||||||
return serviceFail('退款/售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
return serviceFail('退款/售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record.serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_SHIPPED) {
|
||||||
|
const exchangeOrder = await pushExchangeOrder(record)
|
||||||
|
if (!exchangeOrder.success) {
|
||||||
|
return exchangeOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED) {
|
||||||
|
const returnOrder = await pushReturnOrder(record)
|
||||||
|
if (!returnOrder.success) {
|
||||||
|
return returnOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const finish = logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_RECEIVED ||
|
const finish = logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_RECEIVED ||
|
||||||
(record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED)
|
(record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED)
|
||||||
|
const finishReturn = record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER &&
|
||||||
|
logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED
|
||||||
|
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
await tx.supplierAfterSale.update({
|
await tx.supplierAfterSale.update({
|
||||||
@@ -441,6 +589,20 @@ export const afterSaleService = {
|
|||||||
finishTime: finish ? now : undefined
|
finishTime: finish ? now : undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (finishReturn && record.subOrderId) {
|
||||||
|
await tx.supplierOrderSub.updateMany({
|
||||||
|
where: {
|
||||||
|
supplierId,
|
||||||
|
orderId: record.subOrderId,
|
||||||
|
deleted: 0
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
orderState: ORDER_STATE.REFUNDED,
|
||||||
|
orderFinishTime: now,
|
||||||
|
updateTime: now
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return serviceOk({ serviceOrderId, logisticsState, logisticsStateText: logisticsStateText(logisticsState) })
|
return serviceOk({ serviceOrderId, logisticsState, logisticsStateText: logisticsStateText(logisticsState) })
|
||||||
|
|||||||
@@ -76,10 +76,32 @@ const afterSaleStatesByScene = (scene: string) => {
|
|||||||
AFTER_SALE_SERVICE_STATE.RECEIVED
|
AFTER_SALE_SERVICE_STATE.RECEIVED
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
if (scene === 'history') {
|
||||||
|
return [
|
||||||
|
AFTER_SALE_SERVICE_STATE.REJECTED,
|
||||||
|
AFTER_SALE_SERVICE_STATE.FINISHED,
|
||||||
|
AFTER_SALE_SERVICE_STATE.CANCELLED
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pendingShipExcludeAfterSaleStates = [
|
||||||
|
AFTER_SALE_SERVICE_STATE.PENDING_AUDIT,
|
||||||
|
AFTER_SALE_SERVICE_STATE.APPROVED,
|
||||||
|
AFTER_SALE_SERVICE_STATE.USER_SHIPPED,
|
||||||
|
AFTER_SALE_SERVICE_STATE.RECEIVED,
|
||||||
|
AFTER_SALE_SERVICE_STATE.FINISHED
|
||||||
|
]
|
||||||
|
|
||||||
|
const signedOrderStates = new Set<number>([
|
||||||
|
ORDER_STATE.RECEIVED,
|
||||||
|
ORDER_STATE.COMPLETED
|
||||||
|
])
|
||||||
|
|
||||||
|
const isOrderSigned = (orderState?: number | null) => signedOrderStates.has(Number(orderState))
|
||||||
|
|
||||||
export const merchantService = {
|
export const merchantService = {
|
||||||
async listOrders(input: MerchantOrderListInput) {
|
async listOrders(input: MerchantOrderListInput) {
|
||||||
const supplierId = String(input.supplierId || '').trim()
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
@@ -91,7 +113,9 @@ export const merchantService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const orders = await orderDao.findMerchantSubOrders(supplierId, orderState)
|
const orders = await orderDao.findMerchantSubOrders(supplierId, orderState)
|
||||||
const orderItems = orders.map(order => ({
|
const afterSales = await orderDao.findMerchantAfterSales(supplierId, pendingShipExcludeAfterSaleStates)
|
||||||
|
const afterSaleSubOrderIds = new Set(afterSales.map(item => item.subOrderId).filter(Boolean))
|
||||||
|
const orderItems = orders.filter(order => !afterSaleSubOrderIds.has(order.orderId)).map(order => ({
|
||||||
orderId: order.orderId,
|
orderId: order.orderId,
|
||||||
mainOrderId: order.mainOrderId,
|
mainOrderId: order.mainOrderId,
|
||||||
userId: order.userId || '',
|
userId: order.userId || '',
|
||||||
@@ -153,6 +177,7 @@ export const merchantService = {
|
|||||||
serviceState: record.serviceState,
|
serviceState: record.serviceState,
|
||||||
serviceStateText: serviceStateText(record.serviceState),
|
serviceStateText: serviceStateText(record.serviceState),
|
||||||
logisticsState: record.logisticsState,
|
logisticsState: record.logisticsState,
|
||||||
|
isSigned: isOrderSigned(subOrder?.orderState),
|
||||||
reason: record.reason || '',
|
reason: record.reason || '',
|
||||||
problemDescription: record.problemDescription || '',
|
problemDescription: record.problemDescription || '',
|
||||||
pickupMethod: record.pickupMethod || '',
|
pickupMethod: record.pickupMethod || '',
|
||||||
@@ -176,6 +201,10 @@ export const merchantService = {
|
|||||||
return serviceOk({ afterSaleItems })
|
return serviceOk({ afterSaleItems })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
detailAfterSale(input: Pick<AfterSaleAuditInput, 'supplierId' | 'serviceOrderId'>) {
|
||||||
|
return afterSaleService.merchantDetail(input)
|
||||||
|
},
|
||||||
|
|
||||||
auditAfterSale(input: AfterSaleAuditInput) {
|
auditAfterSale(input: AfterSaleAuditInput) {
|
||||||
return afterSaleService.audit(input)
|
return afterSaleService.audit(input)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const statusMap: Record<number, OrderStatusView> = {
|
|||||||
[ORDER_STATE.EXCHANGE]: { text: '换货', statusClass: 'muted', actionText: '查看订单', buttonClass: 'dark-action' },
|
[ORDER_STATE.EXCHANGE]: { text: '换货', statusClass: 'muted', actionText: '查看订单', buttonClass: 'dark-action' },
|
||||||
[ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted', actionText: '查看订单', buttonClass: 'dark-action' }
|
[ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted', actionText: '查看订单', buttonClass: 'dark-action' }
|
||||||
}
|
}
|
||||||
|
const hiddenOrderStates = new Set<number>([ORDER_STATE.EXCHANGE, ORDER_STATE.REFUNDED])
|
||||||
|
|
||||||
const toYuanText = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => {
|
const toYuanText = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => {
|
||||||
if (value && typeof value === 'object' && 'toFixed' in value) {
|
if (value && typeof value === 'object' && 'toFixed' in value) {
|
||||||
@@ -204,6 +205,10 @@ export const orderListService = {
|
|||||||
return serviceFail('供应商或用户信息不能为空')
|
return serviceFail('供应商或用户信息不能为空')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询正在售后中的订单ID列表
|
||||||
|
const processingAfterSales = await orderDao.findProcessingAfterSaleOrderIds(supplierId, userId)
|
||||||
|
const excludeOrderIds = processingAfterSales.map(item => item.subOrderId)
|
||||||
|
|
||||||
const mainOrders = await orderDao.findMainOrdersWithSubs(supplierId, userId)
|
const mainOrders = await orderDao.findMainOrdersWithSubs(supplierId, userId)
|
||||||
const orderItems = mainOrders.flatMap(mainOrder => {
|
const orderItems = mainOrders.flatMap(mainOrder => {
|
||||||
const mainStatus = statusMap[mainOrder.orderState] || statusMap[ORDER_STATE.FINISH_PAY]
|
const mainStatus = statusMap[mainOrder.orderState] || statusMap[ORDER_STATE.FINISH_PAY]
|
||||||
@@ -259,7 +264,9 @@ export const orderListService = {
|
|||||||
products,
|
products,
|
||||||
subOrders
|
subOrders
|
||||||
}]
|
}]
|
||||||
: subOrders.map(subOrder => ({
|
: subOrders
|
||||||
|
.filter(subOrder => !excludeOrderIds.includes(subOrder.orderId) && !hiddenOrderStates.has(Number(subOrder.orderState)))
|
||||||
|
.map(subOrder => ({
|
||||||
...subOrder,
|
...subOrder,
|
||||||
viewId: `sub-${subOrder.orderId}`,
|
viewId: `sub-${subOrder.orderId}`,
|
||||||
displayType: 'sub',
|
displayType: 'sub',
|
||||||
@@ -267,7 +274,10 @@ export const orderListService = {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('order list result', { supplierId, userId, count: orderItems.length })
|
// 过滤掉正在售后中的订单
|
||||||
return serviceOk({ orderItems })
|
const filteredOrderItems = orderItems.filter(item => !excludeOrderIds.includes(item.orderId))
|
||||||
|
|
||||||
|
logger.info('order list result', { supplierId, userId, count: filteredOrderItems.length })
|
||||||
|
return serviceOk({ orderItems: filteredOrderItems })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { supplierDao } from '../dao/supplier.dao.js'
|
import { supplierDao } from '../dao/supplier.dao.js'
|
||||||
import { orderDao } from '../dao/order.dao.js'
|
import { orderDao } from '../dao/order.dao.js'
|
||||||
|
import { ORDER_EVENT_TYPE, ORDER_STATE } from '../constants/order.js'
|
||||||
import { logger } from '../utils/logger.js'
|
import { logger } from '../utils/logger.js'
|
||||||
import { sm4CbcDecrypt, verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
import { sm4CbcDecrypt, verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
||||||
|
|
||||||
@@ -40,6 +41,17 @@ const productSku = (product: { sku: string; subOrderId: string; name: string })
|
|||||||
return `${product.subOrderId}-${product.name || 'SKU'}`
|
return `${product.subOrderId}-${product.name || 'SKU'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toOrderProductInfo = (order: Awaited<ReturnType<typeof orderDao.findOpenApiSubOrders>>[number]) => order.products.map(product => ({
|
||||||
|
sku: productSku(product),
|
||||||
|
number: product.number,
|
||||||
|
price: toMoney(product.price),
|
||||||
|
name: product.name,
|
||||||
|
taxRate: Number(product.taxRate || 0),
|
||||||
|
picInfo: Array.isArray(product.picInfo) ? product.picInfo : product.productImgUrl ? [product.productImgUrl] : [],
|
||||||
|
fileId: product.fileId,
|
||||||
|
filePath: product.filePath
|
||||||
|
}))
|
||||||
|
|
||||||
export const supplierOpenOrderService = {
|
export const supplierOpenOrderService = {
|
||||||
parseBody,
|
parseBody,
|
||||||
|
|
||||||
@@ -67,6 +79,48 @@ export const supplierOpenOrderService = {
|
|||||||
return { resultCode: 401, message: '签名无效或已过期', orderInfo: [] }
|
return { resultCode: 401, message: '签名无效或已过期', orderInfo: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ([ORDER_EVENT_TYPE.RETURN_ORDER, ORDER_EVENT_TYPE.EXCHANGE_ORDER].includes(Number(body.orderType))) {
|
||||||
|
const records = await orderDao.findAfterSales({
|
||||||
|
supplierId,
|
||||||
|
serviceOrderId: { in: orderIds },
|
||||||
|
deleted: 0
|
||||||
|
})
|
||||||
|
const subOrderIds = records.map(item => item.subOrderId).filter(Boolean) as string[]
|
||||||
|
const subOrders = subOrderIds.length ? await orderDao.findOpenApiSubOrders(supplierId, subOrderIds) : []
|
||||||
|
const subOrderMap = new Map(subOrders.map(item => [item.orderId, item]))
|
||||||
|
const orderInfo = records.map(record => {
|
||||||
|
const order = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
orderId: record.serviceOrderId,
|
||||||
|
orderNumberId: order?.orderNumberId || record.serviceOrderId,
|
||||||
|
supplierId: record.supplierId,
|
||||||
|
token: order?.token || '',
|
||||||
|
aplName: order?.aplName || '',
|
||||||
|
createTime: toText(record.applyTime || record.createTime),
|
||||||
|
updateTime: toText(record.updateTime),
|
||||||
|
deliveryTime: toText(order?.deliveryTime),
|
||||||
|
orderFinishTime: toText(record.finishTime),
|
||||||
|
completeTime: toText(record.finishTime),
|
||||||
|
deliveryStatus: order?.deliveryStatus || '',
|
||||||
|
freightCost: toMoney(order?.freightCost),
|
||||||
|
wPayOrAPayFreightCost: toMoney(order?.wPayOrAPayFreightCost),
|
||||||
|
refund: record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER ? toMoney(record.amount) : '0.00',
|
||||||
|
wPayOrAPayRefund: '0.00',
|
||||||
|
logisticsOrderId: order?.logisticsOrderId || '',
|
||||||
|
crrgBsnNm: order?.crrgBsnNm || '',
|
||||||
|
orderState: record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER ? ORDER_STATE.REFUNDED : ORDER_STATE.EXCHANGE,
|
||||||
|
totalPrice: toMoney(record.amount),
|
||||||
|
wPayOrAPay: '0.00',
|
||||||
|
pointsPrice: toMoney(record.amount),
|
||||||
|
invoiceCode: order?.invoiceCode || '',
|
||||||
|
orderProInfos: order ? toOrderProductInfo(order) : []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { resultCode: 0, message: '成功', orderInfo }
|
||||||
|
}
|
||||||
|
|
||||||
const orders = await orderDao.findOpenApiSubOrders(supplierId, orderIds)
|
const orders = await orderDao.findOpenApiSubOrders(supplierId, orderIds)
|
||||||
const orderInfo = orders.map(order => ({
|
const orderInfo = orders.map(order => ({
|
||||||
orderId: order.orderId,
|
orderId: order.orderId,
|
||||||
@@ -91,16 +145,7 @@ export const supplierOpenOrderService = {
|
|||||||
wPayOrAPay: toMoney(order.wPayOrAPay),
|
wPayOrAPay: toMoney(order.wPayOrAPay),
|
||||||
pointsPrice: toMoney(order.pointsPrice),
|
pointsPrice: toMoney(order.pointsPrice),
|
||||||
invoiceCode: order.invoiceCode,
|
invoiceCode: order.invoiceCode,
|
||||||
orderProInfos: order.products.map(product => ({
|
orderProInfos: toOrderProductInfo(order)
|
||||||
sku: productSku(product),
|
|
||||||
number: product.number,
|
|
||||||
price: toMoney(product.price),
|
|
||||||
name: product.name,
|
|
||||||
taxRate: Number(product.taxRate || 0),
|
|
||||||
picInfo: Array.isArray(product.picInfo) ? product.picInfo : product.productImgUrl ? [product.productImgUrl] : [],
|
|
||||||
fileId: product.fileId,
|
|
||||||
filePath: product.filePath
|
|
||||||
}))
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return { resultCode: 0, message: '成功', orderInfo }
|
return { resultCode: 0, message: '成功', orderInfo }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { SupplierInfo } from '@prisma/client'
|
import type { SupplierInfo } from '@prisma/client'
|
||||||
import { supplierDao } from '../dao/supplier.dao.js'
|
import { supplierDao } from '../dao/supplier.dao.js'
|
||||||
import { createBridgeSignature } from './crypto.js'
|
import { createBridgeSignature } from './crypto.js'
|
||||||
|
import { logger } from './logger.js'
|
||||||
import { getRedis } from './redis.js'
|
import { getRedis } from './redis.js'
|
||||||
|
|
||||||
type BridgeTokenResponse = {
|
type BridgeTokenResponse = {
|
||||||
@@ -168,17 +169,25 @@ export const bridgeFetchWithAuth = async <T>(
|
|||||||
|
|
||||||
const call = async (forceRefresh = false) => {
|
const call = async (forceRefresh = false) => {
|
||||||
const token = await getBridgeToken(supplierId, forceRefresh)
|
const token = await getBridgeToken(supplierId, forceRefresh)
|
||||||
|
const requestBody = {
|
||||||
|
...body,
|
||||||
|
supplierId,
|
||||||
|
token
|
||||||
|
}
|
||||||
|
logger.supplierDebug('zhgh-send', '向ZHGH发送请求', {
|
||||||
|
method: 'POST',
|
||||||
|
path,
|
||||||
|
supplierId,
|
||||||
|
url: `${apiBase}${path}`,
|
||||||
|
body: requestBody
|
||||||
|
})
|
||||||
const response = await fetch(`${apiBase}${path}`, {
|
const response = await fetch(`${apiBase}${path}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
token
|
token
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(requestBody)
|
||||||
...body,
|
|
||||||
supplierId,
|
|
||||||
token
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
const payload = (await response.json().catch(() => ({}))) as T
|
const payload = (await response.json().catch(() => ({}))) as T
|
||||||
return { response, payload, token }
|
return { response, payload, token }
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
type LogLevel = 'info' | 'warn' | 'error'
|
type LogLevel = 'info' | 'warn' | 'error'
|
||||||
|
type SupplierLogType = 'backend-receive' | 'zhgh-send' | 'zhgh-receive'
|
||||||
|
|
||||||
|
export type SupplierDebugLog = {
|
||||||
|
id: number
|
||||||
|
type: SupplierLogType
|
||||||
|
title: string
|
||||||
|
time: string
|
||||||
|
content: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplierDebugLogs: SupplierDebugLog[] = []
|
||||||
|
let supplierDebugLogId = 0
|
||||||
|
const MAX_SUPPLIER_DEBUG_LOGS = 200
|
||||||
|
|
||||||
const write = (level: LogLevel, message: string, meta?: unknown) => {
|
const write = (level: LogLevel, message: string, meta?: unknown) => {
|
||||||
const line = `[supplied-service] ${new Date().toISOString()} ${level.toUpperCase()} ${message}`
|
const line = `[supplied-service] ${new Date().toISOString()} ${level.toUpperCase()} ${message}`
|
||||||
@@ -19,5 +32,19 @@ const write = (level: LogLevel, message: string, meta?: unknown) => {
|
|||||||
export const logger = {
|
export const logger = {
|
||||||
info: (message: string, meta?: unknown) => write('info', message, meta),
|
info: (message: string, meta?: unknown) => write('info', message, meta),
|
||||||
warn: (message: string, meta?: unknown) => write('warn', message, meta),
|
warn: (message: string, meta?: unknown) => write('warn', message, meta),
|
||||||
error: (message: string, meta?: unknown) => write('error', message, meta)
|
error: (message: string, meta?: unknown) => write('error', message, meta),
|
||||||
|
supplierDebug: (type: SupplierLogType, title: string, content: unknown) => {
|
||||||
|
supplierDebugLogs.unshift({
|
||||||
|
id: ++supplierDebugLogId,
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
content
|
||||||
|
})
|
||||||
|
supplierDebugLogs.splice(MAX_SUPPLIER_DEBUG_LOGS)
|
||||||
|
},
|
||||||
|
getSupplierDebugLogs: () => supplierDebugLogs,
|
||||||
|
clearSupplierDebugLogs: () => {
|
||||||
|
supplierDebugLogs.splice(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user