feat: 订单售后
This commit is contained in:
@@ -38,3 +38,19 @@ export const postFunction = async (name, body = {}) => {
|
|||||||
|
|
||||||
return payload.data
|
return payload.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const uploadImageFunction = async (file, folder = 'after-sale') => {
|
||||||
|
const contentBase64 = await new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => resolve(String(reader.result || ''))
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
|
||||||
|
return postFunction('upload', {
|
||||||
|
fileName: file.name,
|
||||||
|
contentType: file.type,
|
||||||
|
contentBase64,
|
||||||
|
folder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ import JoyfulSelectionAddressList from '@/view/joyful-selection/address/index.vu
|
|||||||
import JoyfulSelectionAddressEdit from '@/view/joyful-selection/address/edit/index.vue'
|
import JoyfulSelectionAddressEdit from '@/view/joyful-selection/address/edit/index.vue'
|
||||||
import JoyfulSelectionOrders from '@/view/joyful-selection/profile/orders/index.vue'
|
import JoyfulSelectionOrders from '@/view/joyful-selection/profile/orders/index.vue'
|
||||||
import JoyfulSelectionOrderDetail from '@/view/joyful-selection/profile/order-detail/index.vue'
|
import JoyfulSelectionOrderDetail from '@/view/joyful-selection/profile/order-detail/index.vue'
|
||||||
import JoyfulSelectionAfterSale from '@/view/joyful-selection/profile/after-sale/index.vue'
|
import JoyfulSelectionAfterSale from '@/view/joyful-selection/after-sale/index/index.vue'
|
||||||
|
import JoyfulSelectionAfterSaleDetail from '@/view/joyful-selection/after-sale/detail/index.vue'
|
||||||
|
import JoyfulSelectionAfterSaleSelect from '@/view/joyful-selection/after-sale/select/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 JoyfulSelectionMerchant from '@/view/joyful-selection/merchant/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'
|
||||||
@@ -45,6 +50,11 @@ export default new Router({
|
|||||||
{ path: 'orders', component: JoyfulSelectionOrders },
|
{ path: 'orders', component: JoyfulSelectionOrders },
|
||||||
{ path: 'order-detail', component: JoyfulSelectionOrderDetail },
|
{ path: 'order-detail', component: JoyfulSelectionOrderDetail },
|
||||||
{ path: 'after-sale', component: JoyfulSelectionAfterSale },
|
{ path: 'after-sale', component: JoyfulSelectionAfterSale },
|
||||||
|
{ path: 'after-sale/detail', component: JoyfulSelectionAfterSaleDetail },
|
||||||
|
{ path: 'after-sale/select', component: JoyfulSelectionAfterSaleSelect },
|
||||||
|
{ path: 'after-sale/return', component: JoyfulSelectionAfterSaleReturn },
|
||||||
|
{ path: 'after-sale/exchange', component: JoyfulSelectionAfterSaleExchange },
|
||||||
|
{ path: 'merchant', component: JoyfulSelectionMerchant },
|
||||||
{ path: 'profile', component: JoyfulSelectionProfile },
|
{ path: 'profile', component: JoyfulSelectionProfile },
|
||||||
{ path: 'points', component: JoyfulSelectionPoints }
|
{ path: 'points', component: JoyfulSelectionPoints }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ export default {
|
|||||||
...nextAddress,
|
...nextAddress,
|
||||||
addressId: nextAddress.id,
|
addressId: nextAddress.id,
|
||||||
supplierId: this.supplierId,
|
supplierId: this.supplierId,
|
||||||
userId: this.userId
|
userId: this.userId,
|
||||||
|
isDefault: ['checkout', 'after-sale'].includes(this.$route.query.from)
|
||||||
})
|
})
|
||||||
saveChosenAddressId(nextAddress.id)
|
saveChosenAddressId(nextAddress.id)
|
||||||
Toast('保存成功')
|
Toast('保存成功')
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default {
|
|||||||
this.chosenAddressId = item.id
|
this.chosenAddressId = item.id
|
||||||
saveChosenAddressId(item.id)
|
saveChosenAddressId(item.id)
|
||||||
|
|
||||||
if (this.$route.query.from === 'checkout') {
|
if (['checkout', 'after-sale'].includes(this.$route.query.from)) {
|
||||||
const address = this.addresses.find(addressItem => addressItem.id === item.id)
|
const address = this.addresses.find(addressItem => addressItem.id === item.id)
|
||||||
if (address) {
|
if (address) {
|
||||||
try {
|
try {
|
||||||
@@ -101,10 +101,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onAdd() {
|
onAdd() {
|
||||||
this.$router.push('/joyful-selection/address/edit')
|
this.$router.push({ path: '/joyful-selection/address/edit', query: { from: this.$route.query.from } })
|
||||||
},
|
},
|
||||||
onEdit(item) {
|
onEdit(item) {
|
||||||
this.$router.push({ path: '/joyful-selection/address/edit', query: { id: item.id } })
|
this.$router.push({ path: '/joyful-selection/address/edit', query: { id: item.id, from: this.$route.query.from } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,766 @@
|
|||||||
|
<template>
|
||||||
|
<main class="after-sale-detail-page">
|
||||||
|
<van-nav-bar title="售后详情" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
|
<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-else-if="needUserShipNo" class="logistics-row muted" @click="fillLogisticsNo">
|
||||||
|
<span>快递单号</span><b>请填写快递单号</b><van-icon name="arrow" />
|
||||||
|
</p>
|
||||||
|
<em v-if="needUserShipNo">请在7天内填写快递单号,逾期将自动关闭售后申请</em>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="detail-card info-card">
|
||||||
|
<h3>售后信息</h3>
|
||||||
|
<p><span>售后单号</span><b>{{ afterSale.serviceOrderId || '-' }}</b><button type="button" @click="copyServiceNo">复制</button></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><span>取件方式</span><b>{{ pickupMethodText }}</b></p>
|
||||||
|
<p><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" class="bottom-actions">
|
||||||
|
<button v-if="canCancel" type="button">撤销申请</button>
|
||||||
|
<button v-if="needUserShipNo" type="button" class="primary" @click="fillLogisticsNo">填写物流单号</button>
|
||||||
|
<button v-else type="button" class="primary" @click="contactService">联系客服</button>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<van-popup v-model="showLogisticsPopup" round position="bottom">
|
||||||
|
<section class="logistics-popup">
|
||||||
|
<h3>填写物流单号</h3>
|
||||||
|
<van-field v-model="logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
||||||
|
<van-field v-model="logisticsNo" label="快递单号" placeholder="请输入快递单号" />
|
||||||
|
<van-button block class="logistics-submit" :loading="submittingLogistics" @click="submitLogisticsNo">确认提交</van-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: 'JoyfulSelectionAfterSaleDetail',
|
||||||
|
data: () => ({
|
||||||
|
afterSale: {},
|
||||||
|
loading: false,
|
||||||
|
showLogisticsPopup: false,
|
||||||
|
logisticsCompany: '',
|
||||||
|
logisticsNo: '',
|
||||||
|
submittingLogistics: false
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||||
|
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
|
||||||
|
},
|
||||||
|
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 (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 ? '您已寄回商品' : '请在7天内寄回商品',
|
||||||
|
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.isExchange) {
|
||||||
|
steps.push(
|
||||||
|
{
|
||||||
|
key: 'merchantShip',
|
||||||
|
title: '商家寄货',
|
||||||
|
time: this.afterSale.merchantShipTime,
|
||||||
|
desc: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? '商家已寄出换货商品' : '等待商家寄出换货商品',
|
||||||
|
state: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? 'done' : 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 steps.map((step, index) => ({
|
||||||
|
...step,
|
||||||
|
state: serviceState === SERVICE_STATE.REJECTED && index > 1 ? 'todo' : step.state,
|
||||||
|
subTitle: step.state === 'done' ? '' : '-'
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
showReturnInfo() {
|
||||||
|
return Number(this.afterSale.serviceState) >= SERVICE_STATE.APPROVED
|
||||||
|
},
|
||||||
|
returnInfoTitle() {
|
||||||
|
return Number(this.afterSale.logisticsState) >= LOGISTICS_STATE.WAIT_USER_SHIP ? '退货地址' : '取件地址'
|
||||||
|
},
|
||||||
|
needUserShipNo() {
|
||||||
|
return Number(this.afterSale.logisticsState) === LOGISTICS_STATE.WAIT_USER_SHIP && !this.afterSale.userLogisticsNo
|
||||||
|
},
|
||||||
|
canCancel() {
|
||||||
|
return Number(this.afterSale.serviceState) === SERVICE_STATE.PENDING_AUDIT
|
||||||
|
},
|
||||||
|
pickupMethodText() {
|
||||||
|
return this.afterSale.pickupMethod === 'mail' ? '自行寄回' : '上门取件'
|
||||||
|
},
|
||||||
|
pickupAddressText() {
|
||||||
|
return this.afterSale.pickupAddress || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadDetail()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||||
|
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('after-sale-detail', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
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
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fillLogisticsNo() {
|
||||||
|
this.logisticsCompany = this.afterSale.userLogisticsCompany || ''
|
||||||
|
this.logisticsNo = this.afterSale.userLogisticsNo || ''
|
||||||
|
this.showLogisticsPopup = true
|
||||||
|
},
|
||||||
|
async submitLogisticsNo() {
|
||||||
|
if (this.submittingLogistics) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const logisticsNo = String(this.logisticsNo || '').trim()
|
||||||
|
const logisticsCompany = String(this.logisticsCompany || '').trim()
|
||||||
|
if (!logisticsCompany) {
|
||||||
|
Toast('请输入快递公司')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!logisticsNo) {
|
||||||
|
Toast('请输入快递单号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submittingLogistics = true
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
await postFunction('after-sale-logistics', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
serviceOrderId: this.afterSale.serviceOrderId,
|
||||||
|
logisticsState: LOGISTICS_STATE.USER_SHIPPED,
|
||||||
|
userLogisticsCompany: logisticsCompany,
|
||||||
|
userLogisticsNo: logisticsNo
|
||||||
|
})
|
||||||
|
Toast('物流单号已提交')
|
||||||
|
this.showLogisticsPopup = false
|
||||||
|
await this.loadDetail()
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '提交物流单号失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
this.submittingLogistics = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contactService() {
|
||||||
|
Toast('客服功能开发中')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card ::v-deep .van-skeleton + .van-skeleton {
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
display: flex;
|
||||||
|
min-height: 122px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
padding: 18px 22px;
|
||||||
|
border: 1px solid #ffe2d7;
|
||||||
|
background: linear-gradient(135deg, #fff7f2 0%, #fff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.approved,
|
||||||
|
.status-card.finished {
|
||||||
|
border-color: #d7f3e9;
|
||||||
|
background: linear-gradient(135deg, #f2fffa 0%, #fff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.rejected {
|
||||||
|
border-color: #ffe2e2;
|
||||||
|
background: linear-gradient(135deg, #fff5f5 0%, #fff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
display: flex;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: none;
|
||||||
|
margin-right: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28px;
|
||||||
|
background: linear-gradient(180deg, #ff7b2f 0%, #ff4b18 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approved .status-icon,
|
||||||
|
.finished .status-icon {
|
||||||
|
background: linear-gradient(180deg, #41d596 0%, #20b879 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rejected .status-icon {
|
||||||
|
background: linear-gradient(180deg, #ff7676 0%, #f04444 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card h2 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #ff4b18;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approved h2,
|
||||||
|
.finished h2 {
|
||||||
|
color: #20b879;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rejected h2 {
|
||||||
|
color: #f04444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #777;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card button {
|
||||||
|
height: 30px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
padding: 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card h3 {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: #111;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
padding: 2px 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-height: 64px;
|
||||||
|
padding-left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 8px;
|
||||||
|
width: 1px;
|
||||||
|
background: #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item.done::before,
|
||||||
|
.timeline-item.current::before {
|
||||||
|
background: #ff5b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item:last-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #cfd3d8;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item.done .timeline-dot {
|
||||||
|
border-color: #ff5b20;
|
||||||
|
background: #ff5b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item.current .timeline-dot {
|
||||||
|
border: 4px solid #ff5b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-main {
|
||||||
|
width: 126px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-main b {
|
||||||
|
display: block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item.current .timeline-main b {
|
||||||
|
color: #ff4b18;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-main em {
|
||||||
|
display: block;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item > p {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
color: #777;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-thumb {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
flex: none;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 2px 8px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h4 {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info p {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info b {
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row > span {
|
||||||
|
align-self: center;
|
||||||
|
color: #111;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-card p,
|
||||||
|
.info-card p {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-card span,
|
||||||
|
.info-card span {
|
||||||
|
width: 76px;
|
||||||
|
flex: none;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-card b,
|
||||||
|
.info-card b {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: right;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card button {
|
||||||
|
height: 24px;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff0e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-card em {
|
||||||
|
display: block;
|
||||||
|
margin-top: -2px;
|
||||||
|
color: #ff7b2f;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-row {
|
||||||
|
min-height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-row.muted b {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-row .van-icon {
|
||||||
|
flex: none;
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voucher-block h4 {
|
||||||
|
margin: 2px 0 10px;
|
||||||
|
color: #111;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voucher-block > div {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voucher-block .van-image {
|
||||||
|
width: 66px;
|
||||||
|
height: 66px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions button {
|
||||||
|
height: 44px;
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-actions .primary {
|
||||||
|
border-color: #ff4b18;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(90deg, #ff6a2a 0%, #ff4b18 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-popup {
|
||||||
|
padding: 18px 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-popup h3 {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-popup ::v-deep .van-cell {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logistics-submit {
|
||||||
|
height: 42px;
|
||||||
|
margin-top: 18px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(90deg, #ff6a2a 0%, #ff4b18 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,819 @@
|
|||||||
|
<template>
|
||||||
|
<main class="after-sale-return-page">
|
||||||
|
<van-nav-bar title="申请售后(换货)" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
|
<section v-if="loading" class="skeleton-card">
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
<van-skeleton title :row="4" />
|
||||||
|
<van-skeleton title :row="5" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="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 || currentSubOrder.amount || '0.00' }}</b>
|
||||||
|
</div>
|
||||||
|
<span>x{{ item.number || 1 }}</span>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="exchange-card">
|
||||||
|
<div class="exchange-head">
|
||||||
|
<h3>换货商品</h3>
|
||||||
|
<button type="button" @click="openExchangeGoodsPopup">{{ selectedExchangeGoods.sku ? '重新选择' : '选择商品' }}</button>
|
||||||
|
</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 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="method-row">
|
||||||
|
<span>取件方式</span>
|
||||||
|
<button type="button" :class="{ active: pickupMethod === 'door' }" @click="pickupMethod = 'door'">上门取件</button>
|
||||||
|
<button type="button" :class="{ active: pickupMethod === 'mail' }" @click="pickupMethod = 'mail'">自行寄回</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="pickupMethod === 'door'" class="address-row" @click="goAddressList">
|
||||||
|
<span>取件地址</span>
|
||||||
|
<div>
|
||||||
|
<p>{{ receiverNameText }} <b>{{ receiverMobileText }}</b></p>
|
||||||
|
<p>{{ receiverAddressText }}</p>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" />
|
||||||
|
</div>
|
||||||
|
<div class="upload-block">
|
||||||
|
<h3>上传图片(最多3张)</h3>
|
||||||
|
<van-uploader
|
||||||
|
v-model="fileList"
|
||||||
|
class="upload-list"
|
||||||
|
multiple
|
||||||
|
:max-count="3"
|
||||||
|
:after-read="afterRead"
|
||||||
|
@delete="onDeleteImage"
|
||||||
|
/>
|
||||||
|
<p>选填,上传有助于更快处理您的申请</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<van-button v-show="!loading" class="submit-button" block :loading="submitting" @click="submitReturn">提交申请</van-button>
|
||||||
|
<van-popup v-model="showReasonPopup" round position="bottom">
|
||||||
|
<section class="reason-popup">
|
||||||
|
<h3>请选择换货原因</h3>
|
||||||
|
<van-radio-group v-model="selectedReason">
|
||||||
|
<van-cell
|
||||||
|
v-for="item in reasonOptions"
|
||||||
|
:key="item.value"
|
||||||
|
clickable
|
||||||
|
@click="selectReason(item)"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</template>
|
||||||
|
<template #right-icon>
|
||||||
|
<van-radio :name="item.value" checked-color="#ff5b20" />
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-radio-group>
|
||||||
|
</section>
|
||||||
|
</van-popup>
|
||||||
|
<van-popup v-model="showExchangeGoodsPopup" round position="bottom">
|
||||||
|
<section class="exchange-popup">
|
||||||
|
<h3>选择换货商品</h3>
|
||||||
|
<p>仅可选择价格为 ¥{{ originalProductPrice }} 的商品</p>
|
||||||
|
<div class="exchange-goods-list">
|
||||||
|
<article v-for="item in samePriceGoods" :key="item.sku" class="exchange-goods-item" @click="selectExchangeGoods(item)">
|
||||||
|
<van-image class="exchange-goods-thumb" :src="item.image" fit="cover" />
|
||||||
|
<div>
|
||||||
|
<h4>{{ item.name || '-' }}</h4>
|
||||||
|
<p>{{ skuText(item) }}</p>
|
||||||
|
<b>¥{{ item.price }}</b>
|
||||||
|
</div>
|
||||||
|
<van-icon v-if="selectedExchangeGoods.sku === item.sku" name="success" />
|
||||||
|
</article>
|
||||||
|
<van-empty v-if="samePriceGoods.length === 0" description="暂无同价商品" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</van-popup>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapState } from 'pinia'
|
||||||
|
import { Toast } from 'vant'
|
||||||
|
import { getFunction, postFunction, uploadImageFunction } from '@/api/supplied-service'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const ORDER_EVENT_TYPE = {
|
||||||
|
EXCHANGE_ORDER: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_COLOR_MAP = {
|
||||||
|
cream: '奶油白',
|
||||||
|
blue: '雾霾蓝',
|
||||||
|
orange: '暖橙色'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_SIZE_MAP = {
|
||||||
|
standard: '标准款',
|
||||||
|
plus: '升级款'
|
||||||
|
}
|
||||||
|
|
||||||
|
const REASON_OPTIONS = [
|
||||||
|
{ value: 'QUALITY_PROBLEM', label: '商品质量问题' },
|
||||||
|
{ value: 'DAMAGED_GOODS', label: '商品破损' },
|
||||||
|
{ value: 'WRONG_GOODS', label: '发错商品' },
|
||||||
|
{ value: 'SPEC_NOT_FIT', label: '尺寸规格不合适' },
|
||||||
|
{ value: 'DESCRIPTION_MISMATCH', label: '商品与描述不符' },
|
||||||
|
{ value: 'OTHER', label: '其他' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'JoyfulSelectionAfterSaleExchange',
|
||||||
|
data: () => ({
|
||||||
|
loading: false,
|
||||||
|
currentOrder: {},
|
||||||
|
currentSubOrder: {},
|
||||||
|
pickupAddress: {},
|
||||||
|
description: '',
|
||||||
|
pickupMethod: 'door',
|
||||||
|
showReasonPopup: false,
|
||||||
|
showExchangeGoodsPopup: false,
|
||||||
|
selectedReason: '',
|
||||||
|
exchangeGoods: [],
|
||||||
|
selectedExchangeGoods: {},
|
||||||
|
fileList: [],
|
||||||
|
uploadedImages: [],
|
||||||
|
submitting: false
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||||
|
products() {
|
||||||
|
return this.currentSubOrder.products || []
|
||||||
|
},
|
||||||
|
originalProductPrice() {
|
||||||
|
return Number(this.products[0]?.price || this.currentSubOrder.amount || 0).toFixed(2)
|
||||||
|
},
|
||||||
|
samePriceGoods() {
|
||||||
|
return this.exchangeGoods.filter(item => Number(item.price || 0).toFixed(2) === this.originalProductPrice)
|
||||||
|
},
|
||||||
|
receiverNameText() {
|
||||||
|
return this.pickupAddress.name || this.currentSubOrder.receiverName || this.currentOrder.receiverName || '-'
|
||||||
|
},
|
||||||
|
receiverMobileText() {
|
||||||
|
return this.pickupAddress.tel || this.currentSubOrder.receiverMobile || this.currentOrder.receiverMobile || '-'
|
||||||
|
},
|
||||||
|
receiverAddressText() {
|
||||||
|
if (this.pickupAddress.id) {
|
||||||
|
return `${this.pickupAddress.province || ''}${this.pickupAddress.city || ''}${this.pickupAddress.county || ''}${this.pickupAddress.addressDetail || ''}` || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.currentSubOrder.receiverAddress || this.currentOrder.receiverAddress || '-'
|
||||||
|
},
|
||||||
|
reasonOptions() {
|
||||||
|
return REASON_OPTIONS
|
||||||
|
},
|
||||||
|
selectedReasonText() {
|
||||||
|
return this.reasonOptions.find(item => item.value === this.selectedReason)?.label || '请选择售后原因'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadOrderDetail()
|
||||||
|
this.loadPickupAddress()
|
||||||
|
},
|
||||||
|
activated() {
|
||||||
|
this.loadPickupAddress()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||||
|
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(' | ') : '-'
|
||||||
|
},
|
||||||
|
selectReason(item) {
|
||||||
|
this.selectedReason = item.value
|
||||||
|
this.showReasonPopup = false
|
||||||
|
},
|
||||||
|
async openExchangeGoodsPopup() {
|
||||||
|
this.showExchangeGoodsPopup = true
|
||||||
|
if (this.exchangeGoods.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getFunction('product-list')
|
||||||
|
this.exchangeGoods = data?.products || []
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '查询商品列表失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectExchangeGoods(item) {
|
||||||
|
if (Number(item.price || 0).toFixed(2) !== this.originalProductPrice) {
|
||||||
|
Toast('换货商品价格必须与原商品一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedExchangeGoods = item
|
||||||
|
this.showExchangeGoodsPopup = false
|
||||||
|
},
|
||||||
|
async afterRead(fileItems) {
|
||||||
|
const items = Array.isArray(fileItems) ? fileItems : [fileItems]
|
||||||
|
await Promise.all(items.map(async item => {
|
||||||
|
item.status = 'uploading'
|
||||||
|
item.message = '上传中'
|
||||||
|
try {
|
||||||
|
const uploaded = await uploadImageFunction(item.file, 'after-sale')
|
||||||
|
item.url = uploaded.previewUrl || uploaded.url
|
||||||
|
item.objectName = uploaded.objectName
|
||||||
|
item.status = 'done'
|
||||||
|
item.message = ''
|
||||||
|
this.uploadedImages.push({
|
||||||
|
objectName: uploaded.objectName,
|
||||||
|
url: uploaded.previewUrl || uploaded.url
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
item.status = 'failed'
|
||||||
|
item.message = '上传失败'
|
||||||
|
Toast(error.message || '图片上传失败')
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onDeleteImage(item) {
|
||||||
|
this.uploadedImages = this.uploadedImages.filter(image => image.objectName !== item.objectName)
|
||||||
|
},
|
||||||
|
async loadPickupAddress() {
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
if (!this.supplierId || !this.userId) {
|
||||||
|
this.pickupAddress = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await postFunction('address-default', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId
|
||||||
|
})
|
||||||
|
this.pickupAddress = data?.address || {}
|
||||||
|
} catch (error) {
|
||||||
|
this.pickupAddress = {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goAddressList() {
|
||||||
|
this.$router.push({ path: '/joyful-selection/address', query: { from: 'after-sale' } })
|
||||||
|
},
|
||||||
|
async loadOrderDetail() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
const data = await postFunction('order-detail', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
mainOrderId: String(this.$route.query.mainOrderId || ''),
|
||||||
|
orderId: String(this.$route.query.orderId || '')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.currentOrder = data?.order || {}
|
||||||
|
this.currentSubOrder = data?.subOrder || {}
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '查询订单详情失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submitReturn() {
|
||||||
|
if (this.submitting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting = true
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
|
||||||
|
const mainOrderId = this.currentOrder.mainOrderId || this.currentOrder.orderId || this.$route.query.mainOrderId
|
||||||
|
const orderId = this.currentSubOrder.orderId || this.$route.query.orderId
|
||||||
|
if (!this.supplierId || !this.userId || !mainOrderId || !orderId) {
|
||||||
|
Toast('订单信息不完整,请稍后再试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.selectedExchangeGoods.sku) {
|
||||||
|
Toast('请选择换货商品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const applied = await postFunction('after-sale-apply', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
serviceType: ORDER_EVENT_TYPE.EXCHANGE_ORDER,
|
||||||
|
orderId,
|
||||||
|
subOrderId: orderId,
|
||||||
|
mainOrderId,
|
||||||
|
amount: Number(this.currentSubOrder.amount || 0),
|
||||||
|
reason: this.selectedReasonText === '请选择售后原因' ? '换货' : this.selectedReasonText,
|
||||||
|
reasonType: this.selectedReason || 'OTHER',
|
||||||
|
problemDescription: this.description,
|
||||||
|
pickupMethod: this.pickupMethod,
|
||||||
|
pickupName: this.receiverNameText === '-' ? '' : this.receiverNameText,
|
||||||
|
pickupMobile: this.receiverMobileText === '-' ? '' : this.receiverMobileText,
|
||||||
|
pickupAddress: this.receiverAddressText === '-' ? '' : this.receiverAddressText,
|
||||||
|
exchangeProductSku: this.selectedExchangeGoods.sku,
|
||||||
|
exchangeProductName: this.selectedExchangeGoods.name,
|
||||||
|
exchangeProductPrice: this.selectedExchangeGoods.price,
|
||||||
|
images: this.uploadedImages
|
||||||
|
})
|
||||||
|
|
||||||
|
Toast('换货申请已提交,等待商家审核')
|
||||||
|
const serviceOrderId = applied?.serviceOrderId || applied?.afterSale?.serviceOrderId
|
||||||
|
if (serviceOrderId) {
|
||||||
|
this.$router.replace({
|
||||||
|
path: '/joyful-selection/after-sale/detail',
|
||||||
|
query: { serviceOrderId }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$router.back()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '换货申请失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
this.submitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.after-sale-return-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 46px 10px 72px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-sale-return-page ::v-deep .van-nav-bar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card,
|
||||||
|
.skeleton-card,
|
||||||
|
.exchange-card,
|
||||||
|
.form-card {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card h3,
|
||||||
|
.exchange-card h3,
|
||||||
|
.desc-block h3,
|
||||||
|
.upload-block h3 {
|
||||||
|
margin: 0 0 13px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-head h3 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-head button {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid #ff5b20;
|
||||||
|
border-radius: 15px;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-card > p {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-card .product-row {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-thumb {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
flex: none;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 2px 8px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h4 {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info p {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info b {
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row > span {
|
||||||
|
align-self: center;
|
||||||
|
color: #111;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row,
|
||||||
|
.method-row,
|
||||||
|
.address-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 46px;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
color: #222;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row span,
|
||||||
|
.method-row span,
|
||||||
|
.address-row span {
|
||||||
|
flex: none;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row b {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-row {
|
||||||
|
height: 48px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-value {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-value b {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-value .van-icon {
|
||||||
|
flex: none;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row .van-icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-block {
|
||||||
|
padding: 14px 0 12px;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-block textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 70px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-block textarea::placeholder {
|
||||||
|
color: #c2c2c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-block em {
|
||||||
|
display: block;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-row {
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-row span {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-row button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 92px;
|
||||||
|
height: 34px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 16px;
|
||||||
|
background: #f5f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-row button.active {
|
||||||
|
border: 1px solid #ff5b20;
|
||||||
|
color: #ff5b20;
|
||||||
|
background: #fff7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row .van-icon {
|
||||||
|
align-self: center;
|
||||||
|
flex: none;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row > div {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row p {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row b {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-block {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list ::v-deep .van-uploader__wrapper {
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list ::v-deep .van-uploader__upload,
|
||||||
|
.upload-list ::v-deep .van-uploader__preview-image {
|
||||||
|
width: 66px;
|
||||||
|
height: 66px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list ::v-deep .van-uploader__preview {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item {
|
||||||
|
display: flex;
|
||||||
|
width: 66px;
|
||||||
|
height: 66px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item .van-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item span {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-block > p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
right: auto;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 12px;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 44px;
|
||||||
|
background: linear-gradient(90deg, #ff6a2a 0%, #ff4b18 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button ::v-deep .van-button__content {
|
||||||
|
height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 16px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup ::v-deep .van-radio-group {
|
||||||
|
max-height: calc(70vh - 56px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup ::v-deep .van-cell {
|
||||||
|
align-items: center;
|
||||||
|
padding: 13px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup ::v-deep .van-cell__title {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-popup {
|
||||||
|
max-height: 72vh;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 16px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-popup h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-popup > p {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-list {
|
||||||
|
max-height: calc(72vh - 68px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f3f3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-thumb {
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
flex: none;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-item > div {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-item h4 {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 0 5px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-item p {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 0 5px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-item b {
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-goods-item > .van-icon {
|
||||||
|
flex: none;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+69
-5
@@ -3,10 +3,20 @@
|
|||||||
<van-nav-bar title="退款/售后" left-arrow @click-left="$router.back()" />
|
<van-nav-bar title="退款/售后" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
<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-empty v-if="afterSaleItems.length === 0" description="暂无退款/售后" />
|
<van-tabs v-model="activeTab" color="#ff5b20">
|
||||||
|
<van-tab title="处理中" name="processing" />
|
||||||
|
<van-tab title="历史" name="history" />
|
||||||
|
</van-tabs>
|
||||||
|
|
||||||
|
<section class="after-sale-tab-panel">
|
||||||
|
<section v-if="loading" class="skeleton-card">
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
</section>
|
||||||
|
<van-empty v-else-if="currentAfterSaleItems.length === 0" :description="emptyDescription" />
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<article v-for="item in afterSaleItems" :key="item.serviceOrderId" class="after-sale-card">
|
<article v-for="item in currentAfterSaleItems" :key="item.serviceOrderId" class="after-sale-card" @click="goDetail(item)">
|
||||||
<header class="after-sale-card__header">
|
<header class="after-sale-card__header">
|
||||||
<span>{{ item.serviceTypeText }}</span>
|
<span>{{ item.serviceTypeText }}</span>
|
||||||
<b>{{ item.serviceStateText }}</b>
|
<b>{{ item.serviceStateText }}</b>
|
||||||
@@ -21,7 +31,7 @@
|
|||||||
<van-image class="after-sale-thumb" :src="product.image" fit="cover" />
|
<van-image class="after-sale-thumb" :src="product.image" fit="cover" />
|
||||||
<div class="after-sale-product__info">
|
<div class="after-sale-product__info">
|
||||||
<h3>{{ product.name || '-' }}</h3>
|
<h3>{{ product.name || '-' }}</h3>
|
||||||
<p>{{ product.sku || '-' }}</p>
|
<p>{{ skuText(product) }}</p>
|
||||||
<b>¥{{ product.price || item.amount }}</b>
|
<b>¥{{ product.price || item.amount }}</b>
|
||||||
</div>
|
</div>
|
||||||
<span>x{{ product.number || 1 }}</span>
|
<span>x{{ product.number || 1 }}</span>
|
||||||
@@ -36,6 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
</section>
|
||||||
</van-pull-refresh>
|
</van-pull-refresh>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
@@ -46,15 +57,38 @@ 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'
|
||||||
|
|
||||||
|
const SKU_COLOR_MAP = {
|
||||||
|
cream: '奶油白',
|
||||||
|
blue: '雾霾蓝',
|
||||||
|
orange: '暖橙色'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_SIZE_MAP = {
|
||||||
|
standard: '标准款',
|
||||||
|
plus: '升级款'
|
||||||
|
}
|
||||||
|
|
||||||
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']),
|
||||||
|
currentAfterSaleItems() {
|
||||||
|
return this.afterSaleItems.filter(item => {
|
||||||
|
const serviceState = Number(item.serviceState)
|
||||||
|
const isHistory = [2, 5, 6].includes(serviceState)
|
||||||
|
|
||||||
|
return this.activeTab === 'history' ? isHistory : !isHistory
|
||||||
|
})
|
||||||
|
},
|
||||||
|
emptyDescription() {
|
||||||
|
return this.activeTab === 'history' ? '暂无历史售后' : '暂无处理中售后'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadAfterSales()
|
this.loadAfterSales()
|
||||||
@@ -64,6 +98,22 @@ export default {
|
|||||||
previewProducts(products = []) {
|
previewProducts(products = []) {
|
||||||
return products.slice(0, 3)
|
return products.slice(0, 3)
|
||||||
},
|
},
|
||||||
|
skuText(product = {}) {
|
||||||
|
const sku = String(product.sku || product.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(' | ') : '-'
|
||||||
|
},
|
||||||
|
goDetail(item) {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/joyful-selection/after-sale/detail',
|
||||||
|
query: {
|
||||||
|
serviceOrderId: item.serviceOrderId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
async loadAfterSales(showLoading = true) {
|
async loadAfterSales(showLoading = true) {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
@@ -122,7 +172,11 @@ export default {
|
|||||||
.after-sale-list {
|
.after-sale-list {
|
||||||
min-height: calc(100vh - 46px);
|
min-height: calc(100vh - 46px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 12px 10px 18px;
|
padding-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-sale-tab-panel {
|
||||||
|
padding: 12px 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.after-sale-card {
|
.after-sale-card {
|
||||||
@@ -132,6 +186,16 @@ export default {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeleton-card {
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card ::v-deep .van-skeleton + .van-skeleton {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.after-sale-card__header {
|
.after-sale-card__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -0,0 +1,642 @@
|
|||||||
|
<template>
|
||||||
|
<main class="after-sale-return-page">
|
||||||
|
<van-nav-bar title="申请售后(退货)" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
|
<section v-if="loading" class="skeleton-card">
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
<van-skeleton title :row="5" />
|
||||||
|
<van-skeleton title :row="4" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="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 || currentSubOrder.amount || '0.00' }}</b>
|
||||||
|
</div>
|
||||||
|
<span>x{{ item.number || 1 }}</span>
|
||||||
|
</article>
|
||||||
|
</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="method-row">
|
||||||
|
<span>取件方式</span>
|
||||||
|
<button type="button" :class="{ active: pickupMethod === 'door' }" @click="pickupMethod = 'door'">上门取件</button>
|
||||||
|
<button type="button" :class="{ active: pickupMethod === 'mail' }" @click="pickupMethod = 'mail'">自行寄回</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="pickupMethod === 'door'" class="address-row" @click="goAddressList">
|
||||||
|
<span>取件地址</span>
|
||||||
|
<div>
|
||||||
|
<p>{{ receiverNameText }} <b>{{ receiverMobileText }}</b></p>
|
||||||
|
<p>{{ receiverAddressText }}</p>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" />
|
||||||
|
</div>
|
||||||
|
<div class="upload-block">
|
||||||
|
<h3>上传图片(最多3张)</h3>
|
||||||
|
<van-uploader
|
||||||
|
v-model="fileList"
|
||||||
|
class="upload-list"
|
||||||
|
multiple
|
||||||
|
:max-count="3"
|
||||||
|
:after-read="afterRead"
|
||||||
|
@delete="onDeleteImage"
|
||||||
|
/>
|
||||||
|
<p>选填,上传有助于更快处理您的申请</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<van-button v-show="!loading" class="submit-button" block :loading="submitting" @click="submitReturn">提交申请</van-button>
|
||||||
|
<van-popup v-model="showReasonPopup" round position="bottom">
|
||||||
|
<section class="reason-popup">
|
||||||
|
<h3>请选择退货原因</h3>
|
||||||
|
<van-radio-group v-model="selectedReason">
|
||||||
|
<van-cell
|
||||||
|
v-for="item in reasonOptions"
|
||||||
|
:key="item.value"
|
||||||
|
clickable
|
||||||
|
@click="selectReason(item)"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</template>
|
||||||
|
<template #right-icon>
|
||||||
|
<van-radio :name="item.value" checked-color="#ff5b20" />
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-radio-group>
|
||||||
|
</section>
|
||||||
|
</van-popup>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapState } from 'pinia'
|
||||||
|
import { Toast } from 'vant'
|
||||||
|
import { postFunction, uploadImageFunction } from '@/api/supplied-service'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const ORDER_EVENT_TYPE = {
|
||||||
|
RETURN_ORDER: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_COLOR_MAP = {
|
||||||
|
cream: '奶油白',
|
||||||
|
blue: '雾霾蓝',
|
||||||
|
orange: '暖橙色'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_SIZE_MAP = {
|
||||||
|
standard: '标准款',
|
||||||
|
plus: '升级款'
|
||||||
|
}
|
||||||
|
|
||||||
|
const REASON_OPTIONS = [
|
||||||
|
{ value: 'QUALITY_PROBLEM', label: '商品质量问题' },
|
||||||
|
{ value: 'DAMAGED_GOODS', label: '商品破损' },
|
||||||
|
{ value: 'DESCRIPTION_MISMATCH', label: '商品与描述不符' },
|
||||||
|
{ value: 'WRONG_GOODS', label: '发错商品' },
|
||||||
|
{ value: 'MISSING_GOODS', label: '少件/漏发' },
|
||||||
|
{ value: 'SPEC_NOT_FIT', label: '尺寸规格不合适' },
|
||||||
|
{ value: 'DISLIKE', label: '不喜欢/不想要了' },
|
||||||
|
{ value: 'BUY_WRONG', label: '买错了' },
|
||||||
|
{ value: 'OTHER', label: '其他' },
|
||||||
|
{ value: 'SUPPLIER_ERROR', label: '供应商发货错误' },
|
||||||
|
{ value: 'INVOICE_ERROR', label: '发票问题' },
|
||||||
|
{ value: 'DELIVERY_TIMEOUT', label: '未按约定时间发货' },
|
||||||
|
{ value: 'PRICE_ERROR', label: '价格异常' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'JoyfulSelectionAfterSaleReturn',
|
||||||
|
data: () => ({
|
||||||
|
loading: false,
|
||||||
|
currentOrder: {},
|
||||||
|
currentSubOrder: {},
|
||||||
|
pickupAddress: {},
|
||||||
|
description: '',
|
||||||
|
pickupMethod: 'door',
|
||||||
|
showReasonPopup: false,
|
||||||
|
selectedReason: '',
|
||||||
|
fileList: [],
|
||||||
|
uploadedImages: [],
|
||||||
|
submitting: false
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||||
|
products() {
|
||||||
|
return this.currentSubOrder.products || []
|
||||||
|
},
|
||||||
|
receiverNameText() {
|
||||||
|
return this.pickupAddress.name || this.currentSubOrder.receiverName || this.currentOrder.receiverName || '-'
|
||||||
|
},
|
||||||
|
receiverMobileText() {
|
||||||
|
return this.pickupAddress.tel || this.currentSubOrder.receiverMobile || this.currentOrder.receiverMobile || '-'
|
||||||
|
},
|
||||||
|
receiverAddressText() {
|
||||||
|
if (this.pickupAddress.id) {
|
||||||
|
return `${this.pickupAddress.province || ''}${this.pickupAddress.city || ''}${this.pickupAddress.county || ''}${this.pickupAddress.addressDetail || ''}` || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.currentSubOrder.receiverAddress || this.currentOrder.receiverAddress || '-'
|
||||||
|
},
|
||||||
|
reasonOptions() {
|
||||||
|
return REASON_OPTIONS
|
||||||
|
},
|
||||||
|
selectedReasonText() {
|
||||||
|
return this.reasonOptions.find(item => item.value === this.selectedReason)?.label || '请选择售后原因'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadOrderDetail()
|
||||||
|
this.loadPickupAddress()
|
||||||
|
},
|
||||||
|
activated() {
|
||||||
|
this.loadPickupAddress()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||||
|
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(' | ') : '-'
|
||||||
|
},
|
||||||
|
selectReason(item) {
|
||||||
|
this.selectedReason = item.value
|
||||||
|
this.showReasonPopup = false
|
||||||
|
},
|
||||||
|
async afterRead(fileItems) {
|
||||||
|
const items = Array.isArray(fileItems) ? fileItems : [fileItems]
|
||||||
|
await Promise.all(items.map(async item => {
|
||||||
|
item.status = 'uploading'
|
||||||
|
item.message = '上传中'
|
||||||
|
try {
|
||||||
|
const uploaded = await uploadImageFunction(item.file, 'after-sale')
|
||||||
|
item.url = uploaded.previewUrl || uploaded.url
|
||||||
|
item.objectName = uploaded.objectName
|
||||||
|
item.status = 'done'
|
||||||
|
item.message = ''
|
||||||
|
this.uploadedImages.push({
|
||||||
|
objectName: uploaded.objectName,
|
||||||
|
url: uploaded.previewUrl || uploaded.url
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
item.status = 'failed'
|
||||||
|
item.message = '上传失败'
|
||||||
|
Toast(error.message || '图片上传失败')
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onDeleteImage(item) {
|
||||||
|
this.uploadedImages = this.uploadedImages.filter(image => image.objectName !== item.objectName)
|
||||||
|
},
|
||||||
|
async loadPickupAddress() {
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
if (!this.supplierId || !this.userId) {
|
||||||
|
this.pickupAddress = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await postFunction('address-default', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId
|
||||||
|
})
|
||||||
|
this.pickupAddress = data?.address || {}
|
||||||
|
} catch (error) {
|
||||||
|
this.pickupAddress = {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goAddressList() {
|
||||||
|
this.$router.push({ path: '/joyful-selection/address', query: { from: 'after-sale' } })
|
||||||
|
},
|
||||||
|
async loadOrderDetail() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
const data = await postFunction('order-detail', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
mainOrderId: String(this.$route.query.mainOrderId || ''),
|
||||||
|
orderId: String(this.$route.query.orderId || '')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.currentOrder = data?.order || {}
|
||||||
|
this.currentSubOrder = data?.subOrder || {}
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '查询订单详情失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submitReturn() {
|
||||||
|
if (this.submitting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting = true
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
|
||||||
|
const mainOrderId = this.currentOrder.mainOrderId || this.currentOrder.orderId || this.$route.query.mainOrderId
|
||||||
|
const orderId = this.currentSubOrder.orderId || this.$route.query.orderId
|
||||||
|
if (!this.supplierId || !this.userId || !mainOrderId || !orderId) {
|
||||||
|
Toast('订单信息不完整,请稍后再试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const applied = await postFunction('after-sale-apply', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
serviceType: ORDER_EVENT_TYPE.RETURN_ORDER,
|
||||||
|
orderId,
|
||||||
|
subOrderId: orderId,
|
||||||
|
mainOrderId,
|
||||||
|
amount: Number(this.currentSubOrder.amount || 0),
|
||||||
|
reason: this.selectedReasonText === '请选择售后原因' ? '退货' : this.selectedReasonText,
|
||||||
|
reasonType: this.selectedReason || 'OTHER',
|
||||||
|
problemDescription: this.description,
|
||||||
|
pickupMethod: this.pickupMethod,
|
||||||
|
pickupName: this.receiverNameText === '-' ? '' : this.receiverNameText,
|
||||||
|
pickupMobile: this.receiverMobileText === '-' ? '' : this.receiverMobileText,
|
||||||
|
pickupAddress: this.receiverAddressText === '-' ? '' : this.receiverAddressText,
|
||||||
|
images: this.uploadedImages
|
||||||
|
})
|
||||||
|
|
||||||
|
Toast('退货申请已提交,等待商家审核')
|
||||||
|
const serviceOrderId = applied?.serviceOrderId || applied?.afterSale?.serviceOrderId
|
||||||
|
if (serviceOrderId) {
|
||||||
|
this.$router.replace({
|
||||||
|
path: '/joyful-selection/after-sale/detail',
|
||||||
|
query: { serviceOrderId }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$router.back()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '退货申请失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
this.submitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.after-sale-return-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 46px 10px 72px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-sale-return-page ::v-deep .van-nav-bar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card,
|
||||||
|
.skeleton-card,
|
||||||
|
.form-card {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card h3,
|
||||||
|
.desc-block h3,
|
||||||
|
.upload-block h3 {
|
||||||
|
margin: 0 0 13px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-thumb {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
flex: none;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 2px 8px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h4 {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info p {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info b {
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row > span {
|
||||||
|
align-self: center;
|
||||||
|
color: #111;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row,
|
||||||
|
.method-row,
|
||||||
|
.address-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 46px;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
color: #222;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row span,
|
||||||
|
.method-row span,
|
||||||
|
.address-row span {
|
||||||
|
flex: none;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row b {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-row {
|
||||||
|
height: 48px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-value {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-value b {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-value .van-icon {
|
||||||
|
flex: none;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row .van-icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-block {
|
||||||
|
padding: 14px 0 12px;
|
||||||
|
border-bottom: 1px solid #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-block textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 70px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-block textarea::placeholder {
|
||||||
|
color: #c2c2c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-block em {
|
||||||
|
display: block;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-row {
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-row span {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-row button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 92px;
|
||||||
|
height: 34px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 16px;
|
||||||
|
background: #f5f6f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-row button.active {
|
||||||
|
border: 1px solid #ff5b20;
|
||||||
|
color: #ff5b20;
|
||||||
|
background: #fff7f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row .van-icon {
|
||||||
|
align-self: center;
|
||||||
|
flex: none;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row > div {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row p {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-row b {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-block {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list ::v-deep .van-uploader__wrapper {
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list ::v-deep .van-uploader__upload,
|
||||||
|
.upload-list ::v-deep .van-uploader__preview-image {
|
||||||
|
width: 66px;
|
||||||
|
height: 66px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list ::v-deep .van-uploader__preview {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item {
|
||||||
|
display: flex;
|
||||||
|
width: 66px;
|
||||||
|
height: 66px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item .van-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item span {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-block > p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
right: auto;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 12px;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 44px;
|
||||||
|
background: linear-gradient(90deg, #ff6a2a 0%, #ff4b18 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button ::v-deep .van-button__content {
|
||||||
|
height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 16px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup ::v-deep .van-radio-group {
|
||||||
|
max-height: calc(70vh - 56px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup ::v-deep .van-cell {
|
||||||
|
align-items: center;
|
||||||
|
padding: 13px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-popup ::v-deep .van-cell__title {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<template>
|
||||||
|
<main class="after-sale-select-page">
|
||||||
|
<van-nav-bar title="申请售后" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
|
<section v-if="loading" class="skeleton-card">
|
||||||
|
<van-skeleton title :row="3" />
|
||||||
|
<van-skeleton title :row="4" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="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 || currentSubOrder.amount || '0.00' }}</b>
|
||||||
|
</div>
|
||||||
|
<span>x{{ item.number || 1 }}</span>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="!loading" class="type-card">
|
||||||
|
<h3>请选择售后类型</h3>
|
||||||
|
<div class="type-item" @click="goReturn">
|
||||||
|
<span class="type-icon return-icon"><van-icon name="gift-o" /></span>
|
||||||
|
<div>
|
||||||
|
<h4>退货</h4>
|
||||||
|
<p>已收到货,需要退回已收到的商品</p>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" />
|
||||||
|
</div>
|
||||||
|
<div class="type-item" @click="goExchange">
|
||||||
|
<span class="type-icon exchange-icon"><van-icon name="exchange" /></span>
|
||||||
|
<div>
|
||||||
|
<h4>换货</h4>
|
||||||
|
<p>已收到货,需要更换已收到的商品</p>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p v-show="!loading" class="tips">售后申请提交后,商家会尽快为您处理</p>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapState } from 'pinia'
|
||||||
|
import { Toast } from 'vant'
|
||||||
|
import { postFunction } from '@/api/supplied-service'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const SKU_COLOR_MAP = {
|
||||||
|
cream: '奶油白',
|
||||||
|
blue: '雾霾蓝',
|
||||||
|
orange: '暖橙色'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKU_SIZE_MAP = {
|
||||||
|
standard: '标准款',
|
||||||
|
plus: '升级款'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'JoyfulSelectionAfterSaleSelect',
|
||||||
|
data: () => ({
|
||||||
|
loading: false,
|
||||||
|
currentOrder: {},
|
||||||
|
currentSubOrder: {}
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||||
|
products() {
|
||||||
|
return this.currentSubOrder.products || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadOrderDetail()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||||
|
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(' | ') : '-'
|
||||||
|
},
|
||||||
|
async loadOrderDetail() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
const data = await postFunction('order-detail', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
mainOrderId: String(this.$route.query.mainOrderId || ''),
|
||||||
|
orderId: String(this.$route.query.orderId || '')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.currentOrder = data?.order || {}
|
||||||
|
this.currentSubOrder = data?.subOrder || {}
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '查询订单详情失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goReturn() {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/joyful-selection/after-sale/return',
|
||||||
|
query: {
|
||||||
|
mainOrderId: this.currentOrder.mainOrderId || this.$route.query.mainOrderId,
|
||||||
|
orderId: this.currentSubOrder.orderId || this.$route.query.orderId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goExchange() {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/joyful-selection/after-sale/exchange',
|
||||||
|
query: {
|
||||||
|
mainOrderId: this.currentOrder.mainOrderId || this.$route.query.mainOrderId,
|
||||||
|
orderId: this.currentSubOrder.orderId || this.$route.query.orderId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.after-sale-select-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 46px 10px 16px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after-sale-select-page ::v-deep .van-nav-bar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card,
|
||||||
|
.skeleton-card,
|
||||||
|
.type-card {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card h3,
|
||||||
|
.type-card h3 {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-thumb {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
flex: none;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h4 {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info p {
|
||||||
|
margin: 0 0 7px;
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info b {
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row > span {
|
||||||
|
align-self: flex-end;
|
||||||
|
color: #111;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 70px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
display: flex;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.return-icon {
|
||||||
|
background: #ff6b2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-icon {
|
||||||
|
background: #5aa7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-item div {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-item h4 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-item p {
|
||||||
|
margin: 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-item > .van-icon {
|
||||||
|
color: #999;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
padding: 11px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
<template>
|
||||||
|
<main class="merchant-page">
|
||||||
|
<van-nav-bar title="商家处理台" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
|
<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-tab title="待发货" name="ship">
|
||||||
|
<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="orderItems.length === 0" description="暂无待发货订单" />
|
||||||
|
<template v-else>
|
||||||
|
<article v-for="item in orderItems" :key="item.orderId" class="work-card">
|
||||||
|
<header>
|
||||||
|
<b>{{ item.orderNumberId || item.orderId }}</b>
|
||||||
|
<span>待发货</span>
|
||||||
|
</header>
|
||||||
|
<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>{{ product.sku || '-' }}</p>
|
||||||
|
<b>¥{{ product.price }}</b>
|
||||||
|
</div>
|
||||||
|
<em>x{{ product.number || 1 }}</em>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>用户</span><b>{{ item.userId || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>下单时间</span><b>{{ item.createTime || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button type="button" class="primary" @click="openShipPopup(item)">发货</button>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</van-tab>
|
||||||
|
|
||||||
|
<van-tab title="售后审核" name="audit">
|
||||||
|
<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="auditItems.length === 0" description="暂无待审核售后" />
|
||||||
|
<template v-else>
|
||||||
|
<article v-for="item in auditItems" :key="item.serviceOrderId" class="work-card">
|
||||||
|
<header>
|
||||||
|
<b>{{ item.serviceOrderId }}</b>
|
||||||
|
<span>{{ item.serviceTypeText }}</span>
|
||||||
|
</header>
|
||||||
|
<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>{{ product.sku || '-' }}</p>
|
||||||
|
<b>¥{{ product.price }}</b>
|
||||||
|
</div>
|
||||||
|
<em>x{{ product.number || 1 }}</em>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>原因</span><b>{{ item.reason || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>描述</span><b>{{ item.problemDescription || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button type="button" @click="auditAfterSale(item, 2)">拒绝</button>
|
||||||
|
<button type="button" class="primary" @click="auditAfterSale(item, 1)">通过</button>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</van-tab>
|
||||||
|
|
||||||
|
<van-tab title="售后处理" name="process">
|
||||||
|
<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="processItems.length === 0" description="暂无处理中售后" />
|
||||||
|
<template v-else>
|
||||||
|
<article v-for="item in processItems" :key="item.serviceOrderId" class="work-card">
|
||||||
|
<header>
|
||||||
|
<b>{{ item.serviceOrderId }}</b>
|
||||||
|
<span>{{ item.serviceStateText }}</span>
|
||||||
|
</header>
|
||||||
|
<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>{{ product.sku || '-' }}</p>
|
||||||
|
<b>¥{{ product.price }}</b>
|
||||||
|
</div>
|
||||||
|
<em>x{{ product.number || 1 }}</em>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>寄回单号</span><b>{{ item.userLogisticsCompany || '' }} {{ item.userLogisticsNo || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>取件地址</span><b>{{ item.pickupAddress || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button v-if="canReceiveAfterSale(item)" type="button" class="primary" @click="receiveAfterSale(item)">确认收货</button>
|
||||||
|
<button v-if="canShipExchange(item)" type="button" class="primary" @click="openExchangeShipPopup(item)">换货发货</button>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</van-tab>
|
||||||
|
</van-tabs>
|
||||||
|
</van-pull-refresh>
|
||||||
|
|
||||||
|
<van-popup v-model="showShipPopup" round position="bottom">
|
||||||
|
<section class="action-popup">
|
||||||
|
<h3>填写发货信息</h3>
|
||||||
|
<van-field v-model="shipForm.crrgBsnNm" label="快递公司" placeholder="请输入快递公司" />
|
||||||
|
<van-field v-model="shipForm.logisticsOrderId" label="快递单号" placeholder="请输入快递单号" />
|
||||||
|
<button type="button" @click="submitShip">确认发货</button>
|
||||||
|
</section>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<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 ORDER_STATE = {
|
||||||
|
FINISH_PAY: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVICE_TYPE = {
|
||||||
|
EXCHANGE: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVICE_STATE = {
|
||||||
|
APPROVED: 1,
|
||||||
|
USER_SHIPPED: 3,
|
||||||
|
RECEIVED: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'JoyfulSelectionMerchant',
|
||||||
|
data: () => ({
|
||||||
|
loading: false,
|
||||||
|
refreshing: false,
|
||||||
|
activeTab: 'ship',
|
||||||
|
orderItems: [],
|
||||||
|
auditItems: [],
|
||||||
|
processItems: [],
|
||||||
|
currentOrder: null,
|
||||||
|
currentAfterSale: null,
|
||||||
|
showShipPopup: false,
|
||||||
|
showExchangeShipPopup: false,
|
||||||
|
shipForm: {
|
||||||
|
crrgBsnNm: '',
|
||||||
|
logisticsOrderId: ''
|
||||||
|
},
|
||||||
|
exchangeShipForm: {
|
||||||
|
logisticsCompany: '',
|
||||||
|
logisticsNo: ''
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapState(useUserStore, ['supplierId'])
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.prepareSupplier().then(() => this.loadCurrentTab())
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useUserStore, ['fetchSupplierInfo', 'fetchBridgeToken']),
|
||||||
|
async prepareSupplier() {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
if (!this.supplierId) {
|
||||||
|
await this.fetchSupplierInfo()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadCurrentTab(showLoading = true) {
|
||||||
|
const shouldShowLoading = typeof showLoading === 'boolean' ? showLoading : true
|
||||||
|
if (shouldShowLoading) {
|
||||||
|
this.loading = true
|
||||||
|
}
|
||||||
|
if (this.activeTab === 'ship') {
|
||||||
|
await this.loadOrders().finally(() => {
|
||||||
|
if (shouldShowLoading) {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (this.activeTab === 'audit') {
|
||||||
|
await this.loadAfterSales('audit').finally(() => {
|
||||||
|
if (shouldShowLoading) {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await this.loadAfterSales('process').finally(() => {
|
||||||
|
if (shouldShowLoading) {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onRefresh() {
|
||||||
|
try {
|
||||||
|
await this.loadCurrentTab(false)
|
||||||
|
} finally {
|
||||||
|
this.refreshing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadOrders() {
|
||||||
|
const data = await postFunction('merchant-order-list', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
orderState: ORDER_STATE.FINISH_PAY
|
||||||
|
})
|
||||||
|
this.orderItems = data?.orderItems || []
|
||||||
|
},
|
||||||
|
async loadAfterSales(scene) {
|
||||||
|
const data = await postFunction('merchant-after-sale-list', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
scene
|
||||||
|
})
|
||||||
|
if (scene === 'audit') {
|
||||||
|
this.auditItems = data?.afterSaleItems || []
|
||||||
|
} else {
|
||||||
|
this.processItems = data?.afterSaleItems || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openShipPopup(item) {
|
||||||
|
this.currentOrder = item
|
||||||
|
this.shipForm = {
|
||||||
|
crrgBsnNm: item.crrgBsnNm || '',
|
||||||
|
logisticsOrderId: item.logisticsOrderId || ''
|
||||||
|
}
|
||||||
|
this.showShipPopup = true
|
||||||
|
},
|
||||||
|
async submitShip() {
|
||||||
|
if (!this.shipForm.crrgBsnNm || !this.shipForm.logisticsOrderId) {
|
||||||
|
Toast('请填写完整发货信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await postFunction('merchant-order-ship', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.currentOrder.userId,
|
||||||
|
orderId: this.currentOrder.orderId,
|
||||||
|
crrgBsnNm: this.shipForm.crrgBsnNm,
|
||||||
|
logisticsOrderId: this.shipForm.logisticsOrderId
|
||||||
|
})
|
||||||
|
Toast('发货成功')
|
||||||
|
this.showShipPopup = false
|
||||||
|
await this.loadOrders()
|
||||||
|
},
|
||||||
|
async auditAfterSale(item, auditStatus) {
|
||||||
|
await Dialog.confirm({
|
||||||
|
title: auditStatus === 1 ? '确认通过售后申请?' : '确认拒绝售后申请?',
|
||||||
|
message: auditStatus === 1 ? '通过后用户可寄回商品。' : '拒绝后该售后申请将结束。'
|
||||||
|
})
|
||||||
|
await postFunction('merchant-after-sale-audit', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
serviceOrderId: item.serviceOrderId,
|
||||||
|
auditStatus,
|
||||||
|
auditRemark: auditStatus === 1 ? '审核通过' : '商家拒绝'
|
||||||
|
})
|
||||||
|
Toast('处理成功')
|
||||||
|
await this.loadAfterSales('audit')
|
||||||
|
},
|
||||||
|
canReceiveAfterSale(item) {
|
||||||
|
return Number(item.serviceState) === SERVICE_STATE.USER_SHIPPED
|
||||||
|
},
|
||||||
|
canShipExchange(item) {
|
||||||
|
return Number(item.serviceType) === SERVICE_TYPE.EXCHANGE && Number(item.serviceState) === SERVICE_STATE.RECEIVED
|
||||||
|
},
|
||||||
|
async receiveAfterSale(item) {
|
||||||
|
await Dialog.confirm({
|
||||||
|
title: '确认已收到寄回商品?',
|
||||||
|
message: '确认后退货售后会进入完成,换货售后会进入待商家寄货。'
|
||||||
|
})
|
||||||
|
await postFunction('merchant-after-sale-receive', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
serviceOrderId: item.serviceOrderId
|
||||||
|
})
|
||||||
|
Toast('已确认收货')
|
||||||
|
await this.loadAfterSales('process')
|
||||||
|
},
|
||||||
|
openExchangeShipPopup(item) {
|
||||||
|
this.currentAfterSale = item
|
||||||
|
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.currentAfterSale.serviceOrderId,
|
||||||
|
logisticsCompany: this.exchangeShipForm.logisticsCompany,
|
||||||
|
logisticsNo: this.exchangeShipForm.logisticsNo
|
||||||
|
})
|
||||||
|
Toast('换货发货成功')
|
||||||
|
this.showExchangeShipPopup = false
|
||||||
|
await this.loadAfterSales('process')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.merchant-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-top: 46px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-page ::v-deep .van-nav-bar {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merchant-refresh {
|
||||||
|
min-height: calc(100vh - 46px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-panel {
|
||||||
|
min-height: calc(100vh - 90px);
|
||||||
|
padding: 12px 10px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card {
|
||||||
|
padding: 14px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card ::v-deep .van-skeleton + .van-skeleton {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card header b {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card header span {
|
||||||
|
flex: none;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-thumb {
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
flex: none;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h4 {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 0 5px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info p {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 0 5px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info b {
|
||||||
|
color: #ff5b20;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row em {
|
||||||
|
flex: none;
|
||||||
|
color: #222;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row span {
|
||||||
|
width: 64px;
|
||||||
|
flex: none;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row b {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
color: #555;
|
||||||
|
font-weight: 400;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card footer button {
|
||||||
|
min-width: 84px;
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 17px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card footer .primary {
|
||||||
|
border-color: #ff5b20;
|
||||||
|
color: #fff;
|
||||||
|
background: #ff5b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-popup {
|
||||||
|
padding: 18px 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-popup h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-popup button {
|
||||||
|
width: 100%;
|
||||||
|
height: 42px;
|
||||||
|
margin-top: 16px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(90deg, #ff6a2a 0%, #ff4b18 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
<template>
|
|
||||||
<main class="confirm-page">
|
|
||||||
<van-nav-bar title="确认支付" left-arrow @click-left="$router.back()" />
|
|
||||||
|
|
||||||
<section class="amount-card">
|
|
||||||
<p>支付金额</p>
|
|
||||||
<b>¥ {{ amountText }}</b>
|
|
||||||
<span>{{ payTypeText }}</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<van-popup v-model="show" class="pay-popup" round position="bottom" :close-on-click-overlay="false">
|
|
||||||
<article class="pay-panel">
|
|
||||||
<header>
|
|
||||||
<van-icon name="cross" @click="$router.back()" />
|
|
||||||
<h2>确认支付</h2>
|
|
||||||
</header>
|
|
||||||
<section class="pay-summary">
|
|
||||||
<p>支付金额</p>
|
|
||||||
<b>¥ {{ amountText }}</b>
|
|
||||||
</section>
|
|
||||||
<div class="pay-info">
|
|
||||||
<p><span>支付方式</span><b>{{ payTypeText }}</b></p>
|
|
||||||
<p><span>订单编号</span><b>{{ paymentInfo.orderNo || '-' }}</b></p>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<van-button round @click="$router.back()">取消</van-button>
|
|
||||||
<van-button round type="danger" @click="confirmPay">确认支付</van-button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
</van-popup>
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { readPaymentInfo, formatYuan } from '../payment/payment-data'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'JoyfulSelectionPaymentConfirm',
|
|
||||||
data: () => ({
|
|
||||||
show: true,
|
|
||||||
paymentInfo: {}
|
|
||||||
}),
|
|
||||||
computed: {
|
|
||||||
amountText() {
|
|
||||||
return formatYuan(this.paymentInfo.amount)
|
|
||||||
},
|
|
||||||
payTypeText() {
|
|
||||||
const map = {
|
|
||||||
points: '积分支付',
|
|
||||||
wechat: '微信支付',
|
|
||||||
alipay: '支付宝支付'
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[this.$route.query.payType] || '积分支付'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.paymentInfo = readPaymentInfo()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
confirmPay() {
|
|
||||||
this.$router.push({ path: '/joyful-selection/payment-success', query: { payType: this.$route.query.payType } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.confirm-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 46px 12px 0;
|
|
||||||
background: #f7f7f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-page ::v-deep .van-nav-bar {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 20;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-card {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding: 28px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-card p {
|
|
||||||
margin: 0 0 13px;
|
|
||||||
color: #555;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-card b {
|
|
||||||
display: block;
|
|
||||||
color: #222;
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-card span {
|
|
||||||
display: block;
|
|
||||||
margin-top: 12px;
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-popup {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-panel {
|
|
||||||
padding: 0 18px 18px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-panel header {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
height: 52px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-panel header h2 {
|
|
||||||
margin: 0;
|
|
||||||
color: #222;
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-panel header .van-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
color: #999;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-summary {
|
|
||||||
padding: 18px 0 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-summary p {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
color: #777;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-summary b {
|
|
||||||
color: #222;
|
|
||||||
font-size: 30px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-info {
|
|
||||||
padding: 10px 0;
|
|
||||||
border-top: 1px solid #f3f3f3;
|
|
||||||
border-bottom: 1px solid #f3f3f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-info p {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 0;
|
|
||||||
color: #777;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-info b {
|
|
||||||
max-width: 68%;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: right;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-panel footer {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
padding-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-panel footer .van-button {
|
|
||||||
flex: 1;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-page ::v-deep .van-button--danger {
|
|
||||||
background: #ff5b20;
|
|
||||||
border-color: #ff5b20;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import bluetoothEarbudsImage from '@/assets/joyful-selection/products/bluetooth-earbuds.png'
|
|
||||||
import miniOvenImage from '@/assets/joyful-selection/products/mini-oven.png'
|
|
||||||
import smartSpeakerImage from '@/assets/joyful-selection/products/smart-speaker.png'
|
|
||||||
import giftBoxImage from '@/assets/joyful-selection/products/gift-box.png'
|
|
||||||
|
|
||||||
const productImages = {
|
|
||||||
bluetoothEarbuds: bluetoothEarbudsImage,
|
|
||||||
miniOven: miniOvenImage,
|
|
||||||
smartSpeaker: smartSpeakerImage,
|
|
||||||
giftBox: giftBoxImage
|
|
||||||
}
|
|
||||||
|
|
||||||
const createProduct = ({ id, name, desc, detail, price, sales, image, categoryId, category, tag = '赠品' }) => ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
desc,
|
|
||||||
detail: detail || desc,
|
|
||||||
price,
|
|
||||||
originPrice: (Number(price) * 1.25).toFixed(2),
|
|
||||||
sales,
|
|
||||||
image,
|
|
||||||
categoryId,
|
|
||||||
category,
|
|
||||||
tag
|
|
||||||
})
|
|
||||||
|
|
||||||
export const joyfulCategories = [
|
|
||||||
{ id: 'recommend', name: '推荐' },
|
|
||||||
{ id: 'homeLiving', name: '居家生活' },
|
|
||||||
{ id: 'digitalAppliance', name: '数码家电' },
|
|
||||||
{ id: 'beautyCare', name: '美妆个护' },
|
|
||||||
{ id: 'foodDrink', name: '食品饮料' },
|
|
||||||
{ id: 'motherBaby', name: '母婴玩具' },
|
|
||||||
{ id: 'fashionBag', name: '服饰鞋包' },
|
|
||||||
{ id: 'sportsOutdoor', name: '运动户外' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const categoryNameMap = joyfulCategories.reduce((map, category) => {
|
|
||||||
map[category.id] = category.name
|
|
||||||
return map
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const rawProducts = [
|
|
||||||
['cream-oven', '奶油白家用迷你烤箱', '小巧不占地,早餐烘焙好帮手', '129.00', '1.2万件', productImages.miniOven, 'homeLiving'],
|
|
||||||
['cotton-bedding', '100%纯棉舒适四件套', '柔软亲肤,安心睡眠', '179.00', '6453件', productImages.miniOven, 'homeLiving'],
|
|
||||||
['aroma-gift', '橙意香薰礼盒套装', '淡雅留香,装点居家空间', '59.90', '8321件', productImages.giftBox, 'homeLiving'],
|
|
||||||
['desktop-vase', '简约北欧陶瓷花瓶', '清新配色,点亮桌面角落', '39.90', '3180件', productImages.giftBox, 'homeLiving'],
|
|
||||||
['earbuds-air2', '小米蓝牙耳机 Air2 SE', '轻盈半入耳,通勤运动都好听', '69.90', '1.2万件', productImages.bluetoothEarbuds, 'digitalAppliance'],
|
|
||||||
['smart-speaker-pro', '黑曜智能音箱 Pro', '语音唤醒,沉浸听感', '99.00', '2.1万件', productImages.smartSpeaker, 'digitalAppliance'],
|
|
||||||
['electric-toothbrush', '智能声波电动牙刷', '高频洁净,呵护口腔', '129.00', '2.1万件', productImages.bluetoothEarbuds, 'digitalAppliance'],
|
|
||||||
['dual-charger', '双口快充充电器 30W', '小巧便携,多设备快充', '69.00', '5632件', productImages.smartSpeaker, 'digitalAppliance'],
|
|
||||||
['skin-care-box', '植萃保湿护肤礼盒', '水润修护,清爽不粘腻', '99.90', '4820件', productImages.giftBox, 'beautyCare'],
|
|
||||||
['aroma-wash', '香氛洗护旅行套装', '温和清洁,清新留香', '59.90', '7932件', productImages.giftBox, 'beautyCare'],
|
|
||||||
['makeup-mirror', '便携补光化妆镜', '高清镜面,随时补妆', '49.90', '3651件', productImages.smartSpeaker, 'beautyCare'],
|
|
||||||
['hand-cream', '滋润护手霜三支装', '细腻滋养,轻柔香气', '39.90', '5280件', productImages.giftBox, 'beautyCare'],
|
|
||||||
['orange-gift', '橙意惊喜零食礼盒', '精致包装,送礼更有心意', '59.90', '6324件', productImages.giftBox, 'foodDrink'],
|
|
||||||
['coffee-cup', '咖啡随行杯组合', '锁温防漏,醇香随行', '49.90', '3520件', productImages.bluetoothEarbuds, 'foodDrink'],
|
|
||||||
['tea-snack', '下午茶点心礼盒', '多种风味,一盒满足', '69.90', '4891件', productImages.giftBox, 'foodDrink'],
|
|
||||||
['fruit-drink', '轻享果饮礼盒', '清爽酸甜,聚会常备', '79.90', '2976件', productImages.giftBox, 'foodDrink'],
|
|
||||||
['baby-bottle', '安心防摔儿童水杯', '轻巧易握,出门好携带', '39.90', '4250件', productImages.bluetoothEarbuds, 'motherBaby'],
|
|
||||||
['toy-blocks', '益智拼搭积木套装', '色彩柔和,陪伴成长', '89.00', '2186件', productImages.giftBox, 'motherBaby'],
|
|
||||||
['kids-night-light', '柔光儿童陪伴夜灯', '温和光线,安心入睡', '69.90', '3548件', productImages.smartSpeaker, 'motherBaby'],
|
|
||||||
['baby-towel', '纯棉柔软浴巾两件装', '亲肤吸水,日常必备', '59.90', '2761件', productImages.miniOven, 'motherBaby'],
|
|
||||||
['laptop-backpack', '大容量双肩电脑背包', '分区收纳,通勤出行轻松装', '149.00', '1.6万件', productImages.giftBox, 'fashionBag'],
|
|
||||||
['mini-bag', '轻巧通勤斜挎小包', '简洁百搭,日常实用', '79.90', '5321件', productImages.giftBox, 'fashionBag'],
|
|
||||||
['silk-scarf', '柔软撞色丝巾礼盒', '细腻亲肤,点亮穿搭', '69.90', '2680件', productImages.giftBox, 'fashionBag'],
|
|
||||||
['canvas-tote', '简约帆布托特包', '大容量开口,随手能装', '49.90', '6412件', productImages.giftBox, 'fashionBag'],
|
|
||||||
['sport-bottle', '运动便携保温水杯', '长效锁温,户外随行', '49.90', '7342件', productImages.bluetoothEarbuds, 'sportsOutdoor'],
|
|
||||||
['neck-massager', '筋膜放松按摩仪 Mini', '轻巧便携,舒缓酸胀', '99.00', '3921件', productImages.smartSpeaker, 'sportsOutdoor'],
|
|
||||||
['camping-light', '户外便携露营灯', '多档照明,夜晚更安心', '79.00', '2568件', productImages.smartSpeaker, 'sportsOutdoor'],
|
|
||||||
['fitness-band', '弹力健身拉力带套装', '居家训练,轻松塑形', '29.90', '4396件', productImages.miniOven, 'sportsOutdoor']
|
|
||||||
]
|
|
||||||
|
|
||||||
export const joyfulProductList = rawProducts.map(([id, name, desc, price, sales, image, categoryId]) =>
|
|
||||||
createProduct({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
desc,
|
|
||||||
price,
|
|
||||||
sales,
|
|
||||||
image,
|
|
||||||
categoryId,
|
|
||||||
category: categoryNameMap[categoryId]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
export const joyfulProducts = [
|
|
||||||
{
|
|
||||||
id: 'bluetooth-earbuds',
|
|
||||||
name: '小米蓝牙耳机 Air2 SE',
|
|
||||||
desc: '轻盈半入耳,通勤运动都好听',
|
|
||||||
detail: '舒适半入耳设计,配合便携充电盒,日常通勤、运动和在线会议都能保持清晰稳定的聆听体验。',
|
|
||||||
price: '69.90',
|
|
||||||
originPrice: '99.90',
|
|
||||||
category: '数码家电',
|
|
||||||
tag: '积分兑换',
|
|
||||||
image: bluetoothEarbudsImage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mini-oven',
|
|
||||||
name: '奶油白家用迷你烤箱',
|
|
||||||
desc: '小巧不占地,早餐烘焙好帮手',
|
|
||||||
detail: '紧凑台式机身搭配可视化炉门,适合烤吐司、蛋挞和小份点心,让厨房日常更轻松。',
|
|
||||||
price: '129.00',
|
|
||||||
originPrice: '169.00',
|
|
||||||
category: '居家生活',
|
|
||||||
tag: '新品上架',
|
|
||||||
image: miniOvenImage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'smart-speaker',
|
|
||||||
name: '黑曜智能音箱 Pro',
|
|
||||||
desc: '语音唤醒,沉浸听感',
|
|
||||||
detail: '圆柱一体式声学结构,支持语音互动和日常音乐播放,适合卧室、书房与办公桌面使用。',
|
|
||||||
price: '99.00',
|
|
||||||
originPrice: '139.00',
|
|
||||||
category: '数码家电',
|
|
||||||
tag: '热门推荐',
|
|
||||||
image: smartSpeakerImage
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gift-box',
|
|
||||||
name: '橙意惊喜礼盒',
|
|
||||||
desc: '精致包装,送礼更有心意',
|
|
||||||
detail: '亮橙礼盒搭配丝带包装,适合节日福利、积分兑换礼和日常心意礼赠场景。',
|
|
||||||
price: '59.90',
|
|
||||||
originPrice: '89.90',
|
|
||||||
category: '食品饮料',
|
|
||||||
tag: '品质精选',
|
|
||||||
image: giftBoxImage
|
|
||||||
},
|
|
||||||
...joyfulProductList
|
|
||||||
]
|
|
||||||
|
|
||||||
export const getJoyfulProduct = id => joyfulProducts.find(item => item.id === id) || joyfulProducts[0]
|
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<p><span>订单编号</span><b>{{ currentSubOrder.orderNumberId || currentSubOrder.orderId || '-' }}</b></p>
|
<p><span>订单编号</span><b>{{ currentSubOrder.orderNumberId || currentSubOrder.orderId || '-' }}</b></p>
|
||||||
<p><span>下单时间</span><b>{{ currentSubOrder.createTime || currentOrder.createTime || '-' }}</b></p>
|
<p><span>下单时间</span><b>{{ currentSubOrder.createTime || currentOrder.createTime || '-' }}</b></p>
|
||||||
<p><span>支付方式</span><b>{{ payMethodText }}</b></p>
|
<p><span>支付方式</span><b>{{ payMethodText }}</b></p>
|
||||||
<p v-if="!isPendingPay"><span>支付时间</span><b>{{ currentOrder.payTime || '-' }}</b></p>
|
<p v-if="!isPendingPay"><span>支付时间</span><b>{{ payTimeText }}</b></p>
|
||||||
<p><span>商品金额</span><b>¥{{ currentSubOrder.amount }}</b></p>
|
<p><span>商品金额</span><b>¥{{ currentSubOrder.amount }}</b></p>
|
||||||
<p><span>运费</span><b>¥{{ freightText }}</b></p>
|
<p><span>运费</span><b>¥{{ freightText }}</b></p>
|
||||||
<p><span>实付款</span><b class="price">¥{{ paidAmountText }}</b></p>
|
<p><span>实付款</span><b class="price">¥{{ paidAmountText }}</b></p>
|
||||||
@@ -177,6 +177,9 @@ export default {
|
|||||||
freightText() {
|
freightText() {
|
||||||
return Number(this.currentSubOrder?.freightCost || 0).toFixed(2)
|
return Number(this.currentSubOrder?.freightCost || 0).toFixed(2)
|
||||||
},
|
},
|
||||||
|
payTimeText() {
|
||||||
|
return this.currentSubOrder?.payTime || this.currentOrder?.payTime || '-'
|
||||||
|
},
|
||||||
paidAmountText() {
|
paidAmountText() {
|
||||||
return Number(this.currentSubOrder?.amount || 0).toFixed(2)
|
return Number(this.currentSubOrder?.amount || 0).toFixed(2)
|
||||||
},
|
},
|
||||||
@@ -282,7 +285,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action.key === 'after-sale') {
|
if (action.key === 'after-sale') {
|
||||||
this.submitAfterSale(action)
|
this.goAfterSaleSelect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +306,15 @@ export default {
|
|||||||
})
|
})
|
||||||
this.$router.push('/joyful-selection/payment')
|
this.$router.push('/joyful-selection/payment')
|
||||||
},
|
},
|
||||||
|
goAfterSaleSelect() {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/joyful-selection/after-sale/select',
|
||||||
|
query: {
|
||||||
|
mainOrderId: this.currentOrder?.mainOrderId || this.currentOrder?.orderId,
|
||||||
|
orderId: this.currentSubOrder?.orderId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
async cancelMainOrder() {
|
async cancelMainOrder() {
|
||||||
try {
|
try {
|
||||||
await Dialog.confirm({
|
await Dialog.confirm({
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ([ORDER_EVENT_TYPE.RETURN_ORDER, ORDER_EVENT_TYPE.EXCHANGE_ORDER].includes(action.orderType)) {
|
if ([ORDER_EVENT_TYPE.RETURN_ORDER, ORDER_EVENT_TYPE.EXCHANGE_ORDER].includes(action.orderType)) {
|
||||||
this.submitAfterSale(order, action)
|
this.goAfterSaleSelect(order)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +366,15 @@ export default {
|
|||||||
})
|
})
|
||||||
this.$router.push('/joyful-selection/payment')
|
this.$router.push('/joyful-selection/payment')
|
||||||
},
|
},
|
||||||
|
goAfterSaleSelect(order) {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/joyful-selection/after-sale/select',
|
||||||
|
query: {
|
||||||
|
mainOrderId: order.mainOrderId,
|
||||||
|
orderId: order.orderId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
async cancelMainOrder(order) {
|
async cancelMainOrder(order) {
|
||||||
try {
|
try {
|
||||||
await Dialog.confirm({
|
await Dialog.confirm({
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ UPLOAD_TOKEN="replace-me"
|
|||||||
|
|
||||||
# Main points mall bridge API, for example http://localhost/open.
|
# Main points mall bridge API, for example http://localhost/open.
|
||||||
POINTS_MALL_OPEN_API_BASE="http://localhost/open"
|
POINTS_MALL_OPEN_API_BASE="http://localhost/open"
|
||||||
|
|
||||||
# Supplier open API crypto keys used by /supplierOpenApi/pointOrder/queryOrderInfo.
|
|
||||||
SUPPLIER_OPEN_SM4_KEY_HEX="00112233445566778899aabbccddeeff"
|
SUPPLIER_OPEN_SM4_KEY_HEX="00112233445566778899aabbccddeeff"
|
||||||
SUPPLIER_OPEN_SM4_IV_HEX="0102030405060708090a0b0c0d0e0f10"
|
SUPPLIER_OPEN_SM4_IV_HEX="0102030405060708090a0b0c0d0e0f10"
|
||||||
|
|
||||||
@@ -18,3 +16,9 @@ REDIS_HOST="127.0.0.1"
|
|||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
REDIS_PASSWORD=""
|
REDIS_PASSWORD=""
|
||||||
REDIS_DB="9"
|
REDIS_DB="9"
|
||||||
|
|
||||||
|
MINIO_ACCESS_KEY="replace-me"
|
||||||
|
MINIO_SECRET_KEY="replace-me"
|
||||||
|
MINIO_ENDPOINT="http://localhost:9000"
|
||||||
|
MINIO_BUCKET="njupt"
|
||||||
|
MINIO_REGION="us-east-1"
|
||||||
@@ -92,6 +92,7 @@ model SupplierOrderMain {
|
|||||||
remark String? @db.Text
|
remark String? @db.Text
|
||||||
createTime DateTime?
|
createTime DateTime?
|
||||||
updateTime DateTime?
|
updateTime DateTime?
|
||||||
|
payTime DateTime?
|
||||||
deliveryTime DateTime?
|
deliveryTime DateTime?
|
||||||
orderFinishTime DateTime?
|
orderFinishTime DateTime?
|
||||||
completeTime DateTime?
|
completeTime DateTime?
|
||||||
@@ -131,6 +132,7 @@ model SupplierOrderSub {
|
|||||||
remark String? @db.Text
|
remark String? @db.Text
|
||||||
createTime DateTime?
|
createTime DateTime?
|
||||||
updateTime DateTime?
|
updateTime DateTime?
|
||||||
|
payTime DateTime?
|
||||||
deliveryTime DateTime?
|
deliveryTime DateTime?
|
||||||
orderFinishTime DateTime?
|
orderFinishTime DateTime?
|
||||||
completeTime DateTime?
|
completeTime DateTime?
|
||||||
@@ -309,6 +311,13 @@ model SupplierAfterSale {
|
|||||||
logisticsState Int @default(0)
|
logisticsState Int @default(0)
|
||||||
amount Decimal @default(0) @db.Decimal(10, 2)
|
amount Decimal @default(0) @db.Decimal(10, 2)
|
||||||
reason String? @db.Text
|
reason String? @db.Text
|
||||||
|
reasonType String?
|
||||||
|
problemDescription String? @db.Text
|
||||||
|
pickupMethod String?
|
||||||
|
pickupName String?
|
||||||
|
pickupMobile String?
|
||||||
|
pickupAddress String? @db.Text
|
||||||
|
images Json?
|
||||||
auditRemark String? @db.Text
|
auditRemark String? @db.Text
|
||||||
userLogisticsNo String?
|
userLogisticsNo String?
|
||||||
userLogisticsCompany String?
|
userLogisticsCompany String?
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table supplier_order_main add column payTime datetime null;
|
||||||
|
alter table supplier_order_sub add column payTime datetime null;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
alter table supplier_after_sale add column reasonType varchar(191) null;
|
||||||
|
alter table supplier_after_sale add column problemDescription text null;
|
||||||
|
alter table supplier_after_sale add column pickupMethod varchar(191) null;
|
||||||
|
alter table supplier_after_sale add column pickupName varchar(191) null;
|
||||||
|
alter table supplier_after_sale add column pickupMobile varchar(191) null;
|
||||||
|
alter table supplier_after_sale add column pickupAddress text null;
|
||||||
|
alter table supplier_after_sale add column images json null;
|
||||||
@@ -2,6 +2,8 @@ import type { Context } from '../utils/handler.js'
|
|||||||
import { guardMethod } from '../utils/method.js'
|
import { guardMethod } from '../utils/method.js'
|
||||||
import { withErrorBoundary } from '../utils/handler.js'
|
import { withErrorBoundary } from '../utils/handler.js'
|
||||||
import { basicService } from '../services/basic.service.js'
|
import { basicService } from '../services/basic.service.js'
|
||||||
|
import { minioUploadService, type PreviewInput, type UploadInput } from '../services/minio-upload.service.js'
|
||||||
|
import { readJson } from '../utils/request.js'
|
||||||
import { toHttpResponse } from './controller-response.js'
|
import { toHttpResponse } from './controller-response.js'
|
||||||
|
|
||||||
export const loginController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const loginController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
@@ -22,7 +24,14 @@ export const uploadController = withErrorBoundary(async (request: Request, _cont
|
|||||||
const methodResponse = guardMethod(request, ['POST'])
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
if (methodResponse) return methodResponse
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
return toHttpResponse(basicService.upload())
|
return toHttpResponse(await minioUploadService.upload(await readJson<UploadInput>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const uploadPreviewController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
return toHttpResponse(minioUploadService.preview(await readJson<PreviewInput>(request)))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const userInfoController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const userInfoController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { Context } from '../utils/handler.js'
|
||||||
|
import { guardMethod } from '../utils/method.js'
|
||||||
|
import { readJson } from '../utils/request.js'
|
||||||
|
import { withErrorBoundary } from '../utils/handler.js'
|
||||||
|
import {
|
||||||
|
merchantService,
|
||||||
|
type MerchantAfterSaleListInput,
|
||||||
|
type MerchantOrderListInput,
|
||||||
|
type MerchantShipInput
|
||||||
|
} from '../services/merchant.service.js'
|
||||||
|
import type { AfterSaleAuditInput, AfterSaleLogisticsInput } from '../services/after-sale.service.js'
|
||||||
|
import { toHttpResponse } from './controller-response.js'
|
||||||
|
|
||||||
|
type MerchantAfterSaleActionInput = AfterSaleLogisticsInput & {
|
||||||
|
logisticsNo?: string
|
||||||
|
logisticsCompany?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const merchantOrderListController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
return toHttpResponse(await merchantService.listOrders(await readJson<MerchantOrderListInput>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const merchantOrderShipController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
return toHttpResponse(await merchantService.shipOrder(await readJson<MerchantShipInput>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const merchantAfterSaleListController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
return toHttpResponse(await merchantService.listAfterSales(await readJson<MerchantAfterSaleListInput>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const merchantAfterSaleAuditController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
return toHttpResponse(await merchantService.auditAfterSale(await readJson<AfterSaleAuditInput>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const merchantAfterSaleReceiveController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
const body = await readJson<MerchantAfterSaleActionInput>(request)
|
||||||
|
return toHttpResponse(await merchantService.receiveAfterSale(
|
||||||
|
String(body.serviceOrderId || '').trim(),
|
||||||
|
String(body.supplierId || '').trim()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const merchantAfterSaleShipController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
const body = await readJson<MerchantAfterSaleActionInput>(request)
|
||||||
|
return toHttpResponse(await merchantService.shipExchange(
|
||||||
|
String(body.serviceOrderId || '').trim(),
|
||||||
|
String(body.supplierId || '').trim(),
|
||||||
|
String(body.logisticsNo || body.merchantLogisticsNo || '').trim(),
|
||||||
|
String(body.logisticsCompany || body.merchantLogisticsCompany || '').trim()
|
||||||
|
))
|
||||||
|
})
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
afterSaleService,
|
afterSaleService,
|
||||||
type AfterSaleApplyInput,
|
type AfterSaleApplyInput,
|
||||||
type AfterSaleAuditInput,
|
type AfterSaleAuditInput,
|
||||||
|
type AfterSaleDetailInput,
|
||||||
type AfterSaleListInput,
|
type AfterSaleListInput,
|
||||||
type AfterSaleLogisticsInput
|
type AfterSaleLogisticsInput
|
||||||
} from '../services/after-sale.service.js'
|
} from '../services/after-sale.service.js'
|
||||||
@@ -99,6 +100,13 @@ export const afterSaleListController = withErrorBoundary(async (request: Request
|
|||||||
return toHttpResponse(await afterSaleService.list(await readJson<AfterSaleListInput>(request)))
|
return toHttpResponse(await afterSaleService.list(await readJson<AfterSaleListInput>(request)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const afterSaleDetailController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
return toHttpResponse(await afterSaleService.detail(await readJson<AfterSaleDetailInput>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
export const afterSaleAuditController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const afterSaleAuditController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
const methodResponse = guardMethod(request, ['POST'])
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
if (methodResponse) return methodResponse
|
if (methodResponse) return methodResponse
|
||||||
|
|||||||
@@ -137,6 +137,40 @@ export const orderDao = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
findMerchantSubOrders(supplierId: string, orderState?: number) {
|
||||||
|
return prisma.supplierOrderSub.findMany({
|
||||||
|
where: {
|
||||||
|
supplierId,
|
||||||
|
deleted: 0,
|
||||||
|
...(Number.isInteger(orderState) ? { orderState } : {})
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
products: {
|
||||||
|
where: { deleted: 0 },
|
||||||
|
orderBy: { createAt: 'asc' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ createTime: 'desc' },
|
||||||
|
{ createAt: 'desc' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
findMerchantAfterSales(supplierId: string, serviceStates?: number[]) {
|
||||||
|
return prisma.supplierAfterSale.findMany({
|
||||||
|
where: {
|
||||||
|
supplierId,
|
||||||
|
deleted: 0,
|
||||||
|
...(serviceStates?.length ? { serviceState: { in: serviceStates } } : {})
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ createTime: 'desc' },
|
||||||
|
{ id: 'desc' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
findUserAddresses(supplierId: string, userId: string) {
|
findUserAddresses(supplierId: string, userId: string) {
|
||||||
return prisma.supplierUserAddress.findMany({
|
return prisma.supplierUserAddress.findMany({
|
||||||
where: { supplierId, userId, deleted: 0 },
|
where: { supplierId, userId, deleted: 0 },
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { afterSaleDetailController as default } from '../controllers/order.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantAfterSaleAuditController as default } from '../controllers/merchant.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantAfterSaleListController as default } from '../controllers/merchant.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantAfterSaleReceiveController as default } from '../controllers/merchant.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantAfterSaleShipController as default } from '../controllers/merchant.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantOrderListController as default } from '../controllers/merchant.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantOrderShipController as default } from '../controllers/merchant.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { uploadPreviewController as default } from '../controllers/basic.controller.js'
|
||||||
@@ -14,11 +14,13 @@ import {
|
|||||||
cartAddController,
|
cartAddController,
|
||||||
loginController,
|
loginController,
|
||||||
uploadController,
|
uploadController,
|
||||||
|
uploadPreviewController,
|
||||||
userInfoController
|
userInfoController
|
||||||
} from './controllers/basic.controller.js'
|
} from './controllers/basic.controller.js'
|
||||||
import {
|
import {
|
||||||
afterSaleApplyController,
|
afterSaleApplyController,
|
||||||
afterSaleAuditController,
|
afterSaleAuditController,
|
||||||
|
afterSaleDetailController,
|
||||||
afterSaleListController,
|
afterSaleListController,
|
||||||
afterSaleLogisticsController,
|
afterSaleLogisticsController,
|
||||||
orderCancelMainController,
|
orderCancelMainController,
|
||||||
@@ -33,6 +35,14 @@ import {
|
|||||||
orderShipController,
|
orderShipController,
|
||||||
supplierOrderInfoController
|
supplierOrderInfoController
|
||||||
} from './controllers/order.controller.js'
|
} from './controllers/order.controller.js'
|
||||||
|
import {
|
||||||
|
merchantAfterSaleAuditController,
|
||||||
|
merchantAfterSaleListController,
|
||||||
|
merchantAfterSaleReceiveController,
|
||||||
|
merchantAfterSaleShipController,
|
||||||
|
merchantOrderListController,
|
||||||
|
merchantOrderShipController
|
||||||
|
} from './controllers/merchant.controller.js'
|
||||||
import { productDetailController, productListController } from './controllers/product.controller.js'
|
import { productDetailController, productListController } from './controllers/product.controller.js'
|
||||||
import {
|
import {
|
||||||
supplierDebugTokensController,
|
supplierDebugTokensController,
|
||||||
@@ -52,10 +62,17 @@ const handlers: Record<string, FunctionHandler> = {
|
|||||||
'address-save': addressSaveController,
|
'address-save': addressSaveController,
|
||||||
'after-sale-apply': afterSaleApplyController,
|
'after-sale-apply': afterSaleApplyController,
|
||||||
'after-sale-audit': afterSaleAuditController,
|
'after-sale-audit': afterSaleAuditController,
|
||||||
|
'after-sale-detail': afterSaleDetailController,
|
||||||
'after-sale-list': afterSaleListController,
|
'after-sale-list': afterSaleListController,
|
||||||
'after-sale-logistics': afterSaleLogisticsController,
|
'after-sale-logistics': afterSaleLogisticsController,
|
||||||
'cart-add': cartAddController,
|
'cart-add': cartAddController,
|
||||||
login: loginController,
|
login: loginController,
|
||||||
|
'merchant-after-sale-audit': merchantAfterSaleAuditController,
|
||||||
|
'merchant-after-sale-list': merchantAfterSaleListController,
|
||||||
|
'merchant-after-sale-receive': merchantAfterSaleReceiveController,
|
||||||
|
'merchant-after-sale-ship': merchantAfterSaleShipController,
|
||||||
|
'merchant-order-list': merchantOrderListController,
|
||||||
|
'merchant-order-ship': merchantOrderShipController,
|
||||||
'order-cancel-main': orderCancelMainController,
|
'order-cancel-main': orderCancelMainController,
|
||||||
'order-cancel-paid-main': orderCancelPaidMainController,
|
'order-cancel-paid-main': orderCancelPaidMainController,
|
||||||
'order-cancel-sub': orderCancelSubController,
|
'order-cancel-sub': orderCancelSubController,
|
||||||
@@ -74,6 +91,7 @@ const handlers: Record<string, FunctionHandler> = {
|
|||||||
'supplier-points': supplierPointsController,
|
'supplier-points': supplierPointsController,
|
||||||
'supplier-token': supplierTokenController,
|
'supplier-token': supplierTokenController,
|
||||||
upload: uploadController,
|
upload: uploadController,
|
||||||
|
'upload-preview': uploadPreviewController,
|
||||||
'user-info': userInfoController
|
'user-info': userInfoController
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +134,7 @@ const dispatch = (name: string) => async (ctx: Koa.Context) => {
|
|||||||
await writeResponse(ctx, response)
|
await writeResponse(ctx, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(bodyParser())
|
app.use(bodyParser({ jsonLimit: '10mb' }))
|
||||||
app.use(async (ctx, next) => {
|
app.use(async (ctx, next) => {
|
||||||
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
||||||
if (ctx.method === 'OPTIONS') {
|
if (ctx.method === 'OPTIONS') {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ORDER_STATE
|
ORDER_STATE
|
||||||
} 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 { 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'
|
||||||
|
|
||||||
@@ -17,6 +18,16 @@ export type AfterSaleApplyInput = {
|
|||||||
serviceType?: number
|
serviceType?: number
|
||||||
amount?: number
|
amount?: number
|
||||||
reason?: string
|
reason?: string
|
||||||
|
reasonType?: string
|
||||||
|
problemDescription?: string
|
||||||
|
pickupMethod?: string
|
||||||
|
pickupName?: string
|
||||||
|
pickupMobile?: string
|
||||||
|
pickupAddress?: string
|
||||||
|
images?: unknown[]
|
||||||
|
exchangeProductSku?: string
|
||||||
|
exchangeProductName?: string
|
||||||
|
exchangeProductPrice?: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AfterSaleListInput = {
|
export type AfterSaleListInput = {
|
||||||
@@ -24,6 +35,12 @@ export type AfterSaleListInput = {
|
|||||||
userId?: string
|
userId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AfterSaleDetailInput = {
|
||||||
|
supplierId?: string
|
||||||
|
userId?: string
|
||||||
|
serviceOrderId?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AfterSaleAuditInput = {
|
export type AfterSaleAuditInput = {
|
||||||
supplierId?: string
|
supplierId?: string
|
||||||
serviceOrderId?: string
|
serviceOrderId?: string
|
||||||
@@ -111,6 +128,61 @@ const createServiceOrderId = (subOrderId: string, serviceType: number) => {
|
|||||||
return `AS${serviceType}_${subOrderId}_${Date.now()}`
|
return `AS${serviceType}_${subOrderId}_${Date.now()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 products = subOrder?.products.map(product => ({
|
||||||
|
id: product.id,
|
||||||
|
sku: product.sku,
|
||||||
|
name: product.name,
|
||||||
|
number: product.number,
|
||||||
|
price: toYuanText(product.price),
|
||||||
|
image: product.productImgUrl || firstImage(product.picInfo)
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceOrderId: record.serviceOrderId,
|
||||||
|
supplierId: record.supplierId,
|
||||||
|
userId: record.userId,
|
||||||
|
mainOrderId: record.mainOrderId,
|
||||||
|
subOrderId: record.subOrderId,
|
||||||
|
orderId: record.subOrderId,
|
||||||
|
orderNumberId: subOrder?.orderNumberId || '',
|
||||||
|
serviceType: record.serviceType,
|
||||||
|
serviceTypeText: serviceTypeText(record.serviceType),
|
||||||
|
serviceState: record.serviceState,
|
||||||
|
serviceStateText: serviceStateText(record.serviceState),
|
||||||
|
auditStatus: record.auditStatus,
|
||||||
|
logisticsState: record.logisticsState,
|
||||||
|
logisticsStateText: logisticsStateText(record.logisticsState),
|
||||||
|
amount: toYuanText(record.amount),
|
||||||
|
reason: record.reason || '',
|
||||||
|
reasonType: record.reasonType || '',
|
||||||
|
problemDescription: record.problemDescription || '',
|
||||||
|
pickupMethod: record.pickupMethod || '',
|
||||||
|
pickupName: record.pickupName || '',
|
||||||
|
pickupMobile: record.pickupMobile || '',
|
||||||
|
pickupAddress: record.pickupAddress || '',
|
||||||
|
exchangeProduct: requestBody.exchangeProductSku ? {
|
||||||
|
sku: requestBody.exchangeProductSku,
|
||||||
|
name: requestBody.exchangeProductName || '',
|
||||||
|
price: requestBody.exchangeProductPrice || ''
|
||||||
|
} : null,
|
||||||
|
images: Array.isArray(record.images) ? record.images : [],
|
||||||
|
auditRemark: record.auditRemark || '',
|
||||||
|
userLogisticsNo: record.userLogisticsNo || '',
|
||||||
|
userLogisticsCompany: record.userLogisticsCompany || '',
|
||||||
|
merchantLogisticsNo: record.merchantLogisticsNo || '',
|
||||||
|
merchantLogisticsCompany: record.merchantLogisticsCompany || '',
|
||||||
|
applyTime: toDateTimeText(record.applyTime || record.createTime),
|
||||||
|
auditTime: toDateTimeText(record.auditTime),
|
||||||
|
userShipTime: toDateTimeText(record.userShipTime),
|
||||||
|
merchantReceiveTime: toDateTimeText(record.merchantReceiveTime),
|
||||||
|
merchantShipTime: toDateTimeText(record.merchantShipTime),
|
||||||
|
finishTime: toDateTimeText(record.finishTime),
|
||||||
|
products
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const afterSaleService = {
|
export const afterSaleService = {
|
||||||
async apply(input: AfterSaleApplyInput) {
|
async apply(input: AfterSaleApplyInput) {
|
||||||
const supplierId = String(input.supplierId || '').trim()
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
@@ -118,6 +190,14 @@ export const afterSaleService = {
|
|||||||
const subOrderId = String(input.subOrderId || input.orderId || '').trim()
|
const subOrderId = String(input.subOrderId || input.orderId || '').trim()
|
||||||
const serviceType = Number(input.serviceType)
|
const serviceType = Number(input.serviceType)
|
||||||
const reason = String(input.reason || '').trim()
|
const reason = String(input.reason || '').trim()
|
||||||
|
const reasonType = String(input.reasonType || '').trim()
|
||||||
|
const problemDescription = String(input.problemDescription || '').trim()
|
||||||
|
const pickupMethod = String(input.pickupMethod || '').trim()
|
||||||
|
const pickupName = String(input.pickupName || '').trim()
|
||||||
|
const pickupMobile = String(input.pickupMobile || '').trim()
|
||||||
|
const pickupAddress = String(input.pickupAddress || '').trim()
|
||||||
|
const images = Array.isArray(input.images) ? input.images.slice(0, 3) : []
|
||||||
|
const exchangeProductSku = String(input.exchangeProductSku || '').trim()
|
||||||
logger.info('after sale apply', { supplierId, userId, subOrderId, serviceType })
|
logger.info('after sale apply', { supplierId, userId, subOrderId, serviceType })
|
||||||
|
|
||||||
if (!supplierId || !userId || !subOrderId || !serviceType) {
|
if (!supplierId || !userId || !subOrderId || !serviceType) {
|
||||||
@@ -137,6 +217,22 @@ export const afterSaleService = {
|
|||||||
return serviceFail('当前订单状态不能申请退款/售后', 400, 'ORDER_NOT_ALLOW_AFTER_SALE')
|
return serviceFail('当前订单状态不能申请退款/售后', 400, 'ORDER_NOT_ALLOW_AFTER_SALE')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) {
|
||||||
|
if (!exchangeProductSku) {
|
||||||
|
return serviceFail('换货商品不能为空', 400, 'EXCHANGE_PRODUCT_REQUIRED')
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalProduct = subOrder.products[0]
|
||||||
|
const exchangeProduct = await productDao.findEnabledProduct('', exchangeProductSku)
|
||||||
|
if (!originalProduct || !exchangeProduct) {
|
||||||
|
return serviceFail('换货商品不存在或已下架', 404, 'EXCHANGE_PRODUCT_NOT_FOUND')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toYuanText(exchangeProduct.price) !== toYuanText(originalProduct.price)) {
|
||||||
|
return serviceFail('换货商品价格必须与原商品价格一致', 400, 'EXCHANGE_PRODUCT_PRICE_NOT_MATCH')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const activeAfterSale = await orderDao.findAfterSale({
|
const activeAfterSale = await orderDao.findAfterSale({
|
||||||
supplierId,
|
supplierId,
|
||||||
userId,
|
userId,
|
||||||
@@ -166,6 +262,13 @@ export const afterSaleService = {
|
|||||||
logisticsState: AFTER_SALE_LOGISTICS_STATE.NONE,
|
logisticsState: AFTER_SALE_LOGISTICS_STATE.NONE,
|
||||||
amount,
|
amount,
|
||||||
reason: reason || null,
|
reason: reason || null,
|
||||||
|
reasonType: reasonType || null,
|
||||||
|
problemDescription: problemDescription || null,
|
||||||
|
pickupMethod: pickupMethod || null,
|
||||||
|
pickupName: pickupName || null,
|
||||||
|
pickupMobile: pickupMobile || null,
|
||||||
|
pickupAddress: pickupAddress || null,
|
||||||
|
images,
|
||||||
applyTime: now,
|
applyTime: now,
|
||||||
requestBody: input
|
requestBody: input
|
||||||
}
|
}
|
||||||
@@ -196,7 +299,20 @@ export const afterSaleService = {
|
|||||||
serviceState: AFTER_SALE_SERVICE_STATE.PENDING_AUDIT,
|
serviceState: AFTER_SALE_SERVICE_STATE.PENDING_AUDIT,
|
||||||
serviceStateText: serviceStateText(AFTER_SALE_SERVICE_STATE.PENDING_AUDIT),
|
serviceStateText: serviceStateText(AFTER_SALE_SERVICE_STATE.PENDING_AUDIT),
|
||||||
logisticsState: AFTER_SALE_LOGISTICS_STATE.NONE,
|
logisticsState: AFTER_SALE_LOGISTICS_STATE.NONE,
|
||||||
logisticsStateText: logisticsStateText(AFTER_SALE_LOGISTICS_STATE.NONE)
|
logisticsStateText: logisticsStateText(AFTER_SALE_LOGISTICS_STATE.NONE),
|
||||||
|
reason,
|
||||||
|
reasonType,
|
||||||
|
problemDescription,
|
||||||
|
pickupMethod,
|
||||||
|
pickupName,
|
||||||
|
pickupMobile,
|
||||||
|
pickupAddress,
|
||||||
|
exchangeProduct: exchangeProductSku ? {
|
||||||
|
sku: exchangeProductSku,
|
||||||
|
name: input.exchangeProductName || '',
|
||||||
|
price: input.exchangeProductPrice || ''
|
||||||
|
} : null,
|
||||||
|
images
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -222,50 +338,38 @@ export const afterSaleService = {
|
|||||||
const subOrderMap = new Map(subOrders.map(item => [item.orderId, item]))
|
const subOrderMap = new Map(subOrders.map(item => [item.orderId, item]))
|
||||||
const afterSaleItems = records.map(record => {
|
const afterSaleItems = records.map(record => {
|
||||||
const subOrder = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
|
const subOrder = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
|
||||||
const products = subOrder?.products.map(product => ({
|
return afterSaleDetailPayload(record, subOrder)
|
||||||
id: product.id,
|
|
||||||
sku: product.sku,
|
|
||||||
name: product.name,
|
|
||||||
number: product.number,
|
|
||||||
price: toYuanText(product.price),
|
|
||||||
image: product.productImgUrl || firstImage(product.picInfo)
|
|
||||||
})) || []
|
|
||||||
|
|
||||||
return {
|
|
||||||
serviceOrderId: record.serviceOrderId,
|
|
||||||
supplierId: record.supplierId,
|
|
||||||
userId: record.userId,
|
|
||||||
mainOrderId: record.mainOrderId,
|
|
||||||
subOrderId: record.subOrderId,
|
|
||||||
orderId: record.subOrderId,
|
|
||||||
orderNumberId: subOrder?.orderNumberId || '',
|
|
||||||
serviceType: record.serviceType,
|
|
||||||
serviceTypeText: serviceTypeText(record.serviceType),
|
|
||||||
serviceState: record.serviceState,
|
|
||||||
serviceStateText: serviceStateText(record.serviceState),
|
|
||||||
auditStatus: record.auditStatus,
|
|
||||||
logisticsState: record.logisticsState,
|
|
||||||
logisticsStateText: logisticsStateText(record.logisticsState),
|
|
||||||
amount: toYuanText(record.amount),
|
|
||||||
reason: record.reason || '',
|
|
||||||
auditRemark: record.auditRemark || '',
|
|
||||||
userLogisticsNo: record.userLogisticsNo || '',
|
|
||||||
userLogisticsCompany: record.userLogisticsCompany || '',
|
|
||||||
merchantLogisticsNo: record.merchantLogisticsNo || '',
|
|
||||||
merchantLogisticsCompany: record.merchantLogisticsCompany || '',
|
|
||||||
applyTime: toDateTimeText(record.applyTime || record.createTime),
|
|
||||||
auditTime: toDateTimeText(record.auditTime),
|
|
||||||
userShipTime: toDateTimeText(record.userShipTime),
|
|
||||||
merchantReceiveTime: toDateTimeText(record.merchantReceiveTime),
|
|
||||||
merchantShipTime: toDateTimeText(record.merchantShipTime),
|
|
||||||
finishTime: toDateTimeText(record.finishTime),
|
|
||||||
products
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return serviceOk({ afterSaleItems })
|
return serviceOk({ afterSaleItems })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async detail(input: AfterSaleDetailInput) {
|
||||||
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
|
const userId = String(input.userId || '').trim()
|
||||||
|
const serviceOrderId = String(input.serviceOrderId || '').trim()
|
||||||
|
logger.info('after sale detail query', { supplierId, userId, serviceOrderId })
|
||||||
|
|
||||||
|
if (!supplierId || !userId || !serviceOrderId) {
|
||||||
|
return serviceFail('供应商、用户或售后单不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = await orderDao.findAfterSale({
|
||||||
|
supplierId,
|
||||||
|
userId,
|
||||||
|
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()
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import {
|
||||||
|
AFTER_SALE_LOGISTICS_STATE,
|
||||||
|
AFTER_SALE_SERVICE_STATE,
|
||||||
|
ORDER_EVENT_TYPE,
|
||||||
|
ORDER_STATE
|
||||||
|
} from '../constants/order.js'
|
||||||
|
import { orderDao } from '../dao/order.dao.js'
|
||||||
|
import { afterSaleService, type AfterSaleAuditInput, type AfterSaleLogisticsInput } from './after-sale.service.js'
|
||||||
|
import { orderActionService } from './order-action.service.js'
|
||||||
|
import { logger } from '../utils/logger.js'
|
||||||
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
|
export type MerchantOrderListInput = {
|
||||||
|
supplierId?: string
|
||||||
|
orderState?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MerchantShipInput = {
|
||||||
|
supplierId?: string
|
||||||
|
userId?: string
|
||||||
|
orderId?: string
|
||||||
|
logisticsOrderId?: string
|
||||||
|
crrgBsnNm?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MerchantAfterSaleListInput = {
|
||||||
|
supplierId?: string
|
||||||
|
scene?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceStateText = (serviceState: number) => {
|
||||||
|
if (serviceState === AFTER_SALE_SERVICE_STATE.PENDING_AUDIT) return '商家待审核'
|
||||||
|
if (serviceState === AFTER_SALE_SERVICE_STATE.APPROVED) return '审核通过'
|
||||||
|
if (serviceState === AFTER_SALE_SERVICE_STATE.REJECTED) return '审核拒绝'
|
||||||
|
if (serviceState === AFTER_SALE_SERVICE_STATE.USER_SHIPPED) return '用户已寄回'
|
||||||
|
if (serviceState === AFTER_SALE_SERVICE_STATE.RECEIVED) return '商家已收货'
|
||||||
|
if (serviceState === AFTER_SALE_SERVICE_STATE.FINISHED) return '售后完成'
|
||||||
|
if (serviceState === AFTER_SALE_SERVICE_STATE.CANCELLED) return '已撤销'
|
||||||
|
return '处理中'
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceTypeText = (serviceType: number) => {
|
||||||
|
if (serviceType === ORDER_EVENT_TYPE.RETURN_ORDER) return '退货'
|
||||||
|
if (serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) return '换货'
|
||||||
|
return '售后'
|
||||||
|
}
|
||||||
|
|
||||||
|
const toYuanText = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => {
|
||||||
|
if (value && typeof value === 'object' && 'toFixed' in value) {
|
||||||
|
return value.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(value || 0).toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toDateTimeText = (date?: Date | null) => {
|
||||||
|
if (!date) return ''
|
||||||
|
const pad = (value: number) => String(value).padStart(2, '0')
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstImage = (picInfo: unknown) => {
|
||||||
|
if (Array.isArray(picInfo)) {
|
||||||
|
return String(picInfo[0] || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterSaleStatesByScene = (scene: string) => {
|
||||||
|
if (scene === 'audit') return [AFTER_SALE_SERVICE_STATE.PENDING_AUDIT]
|
||||||
|
if (scene === 'process') {
|
||||||
|
return [
|
||||||
|
AFTER_SALE_SERVICE_STATE.APPROVED,
|
||||||
|
AFTER_SALE_SERVICE_STATE.USER_SHIPPED,
|
||||||
|
AFTER_SALE_SERVICE_STATE.RECEIVED
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const merchantService = {
|
||||||
|
async listOrders(input: MerchantOrderListInput) {
|
||||||
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
|
const orderState = Number(input.orderState ?? ORDER_STATE.FINISH_PAY)
|
||||||
|
logger.info('merchant order list query', { supplierId, orderState })
|
||||||
|
|
||||||
|
if (!supplierId) {
|
||||||
|
return serviceFail('供应商不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
const orders = await orderDao.findMerchantSubOrders(supplierId, orderState)
|
||||||
|
const orderItems = orders.map(order => ({
|
||||||
|
orderId: order.orderId,
|
||||||
|
mainOrderId: order.mainOrderId,
|
||||||
|
userId: order.userId || '',
|
||||||
|
supplierId: order.supplierId,
|
||||||
|
orderState: order.orderState,
|
||||||
|
orderNumberId: order.orderNumberId || '',
|
||||||
|
createTime: toDateTimeText(order.createTime || order.createAt),
|
||||||
|
amount: toYuanText(order.totalPrice),
|
||||||
|
logisticsOrderId: order.logisticsOrderId || '',
|
||||||
|
crrgBsnNm: order.crrgBsnNm || '',
|
||||||
|
products: order.products.map(product => ({
|
||||||
|
id: product.id,
|
||||||
|
sku: product.sku,
|
||||||
|
name: product.name,
|
||||||
|
number: product.number,
|
||||||
|
price: toYuanText(product.price),
|
||||||
|
image: product.productImgUrl || firstImage(product.picInfo)
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
return serviceOk({ orderItems })
|
||||||
|
},
|
||||||
|
|
||||||
|
shipOrder(input: MerchantShipInput) {
|
||||||
|
return orderActionService.shipOrder({
|
||||||
|
supplierId: String(input.supplierId || '').trim(),
|
||||||
|
userId: String(input.userId || '').trim(),
|
||||||
|
orderId: String(input.orderId || '').trim(),
|
||||||
|
deliveryStatus: 1,
|
||||||
|
logisticsOrderId: String(input.logisticsOrderId || '').trim(),
|
||||||
|
crrgBsnNm: String(input.crrgBsnNm || '').trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async listAfterSales(input: MerchantAfterSaleListInput) {
|
||||||
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
|
const scene = String(input.scene || '').trim()
|
||||||
|
logger.info('merchant after sale list query', { supplierId, scene })
|
||||||
|
|
||||||
|
if (!supplierId) {
|
||||||
|
return serviceFail('供应商不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = await orderDao.findMerchantAfterSales(supplierId, afterSaleStatesByScene(scene))
|
||||||
|
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 afterSaleItems = records.map(record => {
|
||||||
|
const subOrder = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceOrderId: record.serviceOrderId,
|
||||||
|
supplierId: record.supplierId,
|
||||||
|
userId: record.userId || '',
|
||||||
|
mainOrderId: record.mainOrderId || '',
|
||||||
|
subOrderId: record.subOrderId || '',
|
||||||
|
serviceType: record.serviceType,
|
||||||
|
serviceTypeText: serviceTypeText(record.serviceType),
|
||||||
|
serviceState: record.serviceState,
|
||||||
|
serviceStateText: serviceStateText(record.serviceState),
|
||||||
|
logisticsState: record.logisticsState,
|
||||||
|
reason: record.reason || '',
|
||||||
|
problemDescription: record.problemDescription || '',
|
||||||
|
pickupMethod: record.pickupMethod || '',
|
||||||
|
pickupName: record.pickupName || '',
|
||||||
|
pickupMobile: record.pickupMobile || '',
|
||||||
|
pickupAddress: record.pickupAddress || '',
|
||||||
|
userLogisticsNo: record.userLogisticsNo || '',
|
||||||
|
userLogisticsCompany: record.userLogisticsCompany || '',
|
||||||
|
applyTime: toDateTimeText(record.applyTime || record.createTime),
|
||||||
|
products: subOrder?.products.map(product => ({
|
||||||
|
id: product.id,
|
||||||
|
sku: product.sku,
|
||||||
|
name: product.name,
|
||||||
|
number: product.number,
|
||||||
|
price: toYuanText(product.price),
|
||||||
|
image: product.productImgUrl || firstImage(product.picInfo)
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return serviceOk({ afterSaleItems })
|
||||||
|
},
|
||||||
|
|
||||||
|
auditAfterSale(input: AfterSaleAuditInput) {
|
||||||
|
return afterSaleService.audit(input)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateAfterSaleLogistics(input: AfterSaleLogisticsInput) {
|
||||||
|
return afterSaleService.updateLogistics(input)
|
||||||
|
},
|
||||||
|
|
||||||
|
receiveAfterSale(serviceOrderId: string, supplierId: string) {
|
||||||
|
return afterSaleService.updateLogistics({
|
||||||
|
supplierId,
|
||||||
|
serviceOrderId,
|
||||||
|
logisticsState: AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
shipExchange(serviceOrderId: string, supplierId: string, logisticsNo: string, logisticsCompany: string) {
|
||||||
|
return afterSaleService.updateLogistics({
|
||||||
|
supplierId,
|
||||||
|
serviceOrderId,
|
||||||
|
logisticsState: AFTER_SALE_LOGISTICS_STATE.MERCHANT_SHIPPED,
|
||||||
|
merchantLogisticsNo: logisticsNo,
|
||||||
|
merchantLogisticsCompany: logisticsCompany
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import crypto from 'node:crypto'
|
||||||
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
|
export type UploadInput = {
|
||||||
|
fileName?: string
|
||||||
|
contentType?: string
|
||||||
|
contentBase64?: string
|
||||||
|
folder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PreviewInput = {
|
||||||
|
objectName?: string
|
||||||
|
expires?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const minioConfig = {
|
||||||
|
accessKey: process.env.MINIO_ACCESS_KEY || '',
|
||||||
|
secretKey: process.env.MINIO_SECRET_KEY || '',
|
||||||
|
endPoint: process.env.MINIO_ENDPOINT || 'http://localhost:9000',
|
||||||
|
bucket: process.env.MINIO_BUCKET || 'njupt',
|
||||||
|
region: process.env.MINIO_REGION || 'us-east-1'
|
||||||
|
}
|
||||||
|
|
||||||
|
const amzDate = (date: Date) => date.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
||||||
|
const shortDate = (date: Date) => amzDate(date).slice(0, 8)
|
||||||
|
const sha256 = (value: crypto.BinaryLike) => crypto.createHash('sha256').update(value).digest('hex')
|
||||||
|
const hmac = (key: crypto.BinaryLike | crypto.KeyObject, value: string) => crypto.createHmac('sha256', key).update(value).digest()
|
||||||
|
const encodePath = (value: string) => value.split('/').map(item => encodeURIComponent(item)).join('/')
|
||||||
|
|
||||||
|
const ensureConfig = () => Boolean(minioConfig.accessKey && minioConfig.secretKey && minioConfig.endPoint && minioConfig.bucket)
|
||||||
|
|
||||||
|
const signingKey = (date: string) => {
|
||||||
|
const dateKey = hmac(`AWS4${minioConfig.secretKey}`, date)
|
||||||
|
const regionKey = hmac(dateKey, minioConfig.region)
|
||||||
|
const serviceKey = hmac(regionKey, 's3')
|
||||||
|
return hmac(serviceKey, 'aws4_request')
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectUrl = (objectName: string) => {
|
||||||
|
const endpoint = new URL(minioConfig.endPoint)
|
||||||
|
endpoint.pathname = `${endpoint.pathname.replace(/\/$/, '')}/${minioConfig.bucket}/${encodePath(objectName)}`
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
const extName = (fileName: string) => {
|
||||||
|
const matched = fileName.match(/\.[a-zA-Z0-9]+$/)
|
||||||
|
return matched ? matched[0].toLowerCase() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeFolder = (folder?: string) => String(folder || 'after-sale').replace(/[^a-zA-Z0-9/_-]/g, '').replace(/^\/+|\/+$/g, '') || 'after-sale'
|
||||||
|
|
||||||
|
const presign = (method: 'GET', objectName: string, expires = 3600) => {
|
||||||
|
const now = new Date()
|
||||||
|
const date = shortDate(now)
|
||||||
|
const fullDate = amzDate(now)
|
||||||
|
const credentialScope = `${date}/${minioConfig.region}/s3/aws4_request`
|
||||||
|
const endpoint = objectUrl(objectName)
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||||
|
'X-Amz-Credential': `${minioConfig.accessKey}/${credentialScope}`,
|
||||||
|
'X-Amz-Date': fullDate,
|
||||||
|
'X-Amz-Expires': String(Math.min(Math.max(Number(expires) || 3600, 1), 604800)),
|
||||||
|
'X-Amz-SignedHeaders': 'host'
|
||||||
|
})
|
||||||
|
const canonicalRequest = [
|
||||||
|
method,
|
||||||
|
endpoint.pathname,
|
||||||
|
params.toString(),
|
||||||
|
`host:${endpoint.host}\n`,
|
||||||
|
'host',
|
||||||
|
'UNSIGNED-PAYLOAD'
|
||||||
|
].join('\n')
|
||||||
|
const stringToSign = [
|
||||||
|
'AWS4-HMAC-SHA256',
|
||||||
|
fullDate,
|
||||||
|
credentialScope,
|
||||||
|
sha256(canonicalRequest)
|
||||||
|
].join('\n')
|
||||||
|
const signature = crypto.createHmac('sha256', signingKey(date)).update(stringToSign).digest('hex')
|
||||||
|
params.set('X-Amz-Signature', signature)
|
||||||
|
endpoint.search = params.toString()
|
||||||
|
return endpoint.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedPutHeaders = (url: URL, body: Buffer, contentType: string) => {
|
||||||
|
const now = new Date()
|
||||||
|
const date = shortDate(now)
|
||||||
|
const fullDate = amzDate(now)
|
||||||
|
const payloadHash = sha256(body)
|
||||||
|
const credentialScope = `${date}/${minioConfig.region}/s3/aws4_request`
|
||||||
|
const canonicalHeaders = [
|
||||||
|
`content-type:${contentType}`,
|
||||||
|
`host:${url.host}`,
|
||||||
|
`x-amz-content-sha256:${payloadHash}`,
|
||||||
|
`x-amz-date:${fullDate}`
|
||||||
|
].join('\n') + '\n'
|
||||||
|
const signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date'
|
||||||
|
const canonicalRequest = [
|
||||||
|
'PUT',
|
||||||
|
url.pathname,
|
||||||
|
'',
|
||||||
|
canonicalHeaders,
|
||||||
|
signedHeaders,
|
||||||
|
payloadHash
|
||||||
|
].join('\n')
|
||||||
|
const stringToSign = [
|
||||||
|
'AWS4-HMAC-SHA256',
|
||||||
|
fullDate,
|
||||||
|
credentialScope,
|
||||||
|
sha256(canonicalRequest)
|
||||||
|
].join('\n')
|
||||||
|
const signature = crypto.createHmac('sha256', signingKey(date)).update(stringToSign).digest('hex')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'X-Amz-Content-Sha256': payloadHash,
|
||||||
|
'X-Amz-Date': fullDate,
|
||||||
|
Authorization: `AWS4-HMAC-SHA256 Credential=${minioConfig.accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const minioUploadService = {
|
||||||
|
async upload(input: UploadInput) {
|
||||||
|
if (!ensureConfig()) {
|
||||||
|
return serviceFail('MinIO配置不完整', 500, 'MINIO_CONFIG_MISSING')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = String(input.fileName || '').trim()
|
||||||
|
const contentType = String(input.contentType || 'application/octet-stream').trim()
|
||||||
|
const contentBase64 = String(input.contentBase64 || '').replace(/^data:[^;]+;base64,/, '')
|
||||||
|
if (!fileName || !contentBase64) {
|
||||||
|
return serviceFail('文件名或图片内容不能为空')
|
||||||
|
}
|
||||||
|
if (!contentType.startsWith('image/')) {
|
||||||
|
return serviceFail('只支持上传图片', 400, 'ONLY_IMAGE_ALLOWED')
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = Buffer.from(contentBase64, 'base64')
|
||||||
|
const objectName = `${safeFolder(input.folder)}/${Date.now()}_${crypto.randomUUID()}${extName(fileName)}`
|
||||||
|
const url = objectUrl(objectName)
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: signedPutHeaders(url, body, contentType),
|
||||||
|
body
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
return serviceFail(`图片上传失败:${response.status}`, 500, 'MINIO_UPLOAD_FAILED')
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewUrl = presign('GET', objectName)
|
||||||
|
return serviceOk({
|
||||||
|
objectName,
|
||||||
|
fileName,
|
||||||
|
contentType,
|
||||||
|
size: body.length,
|
||||||
|
url: previewUrl,
|
||||||
|
previewUrl
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
preview(input: PreviewInput) {
|
||||||
|
if (!ensureConfig()) {
|
||||||
|
return serviceFail('MinIO配置不完整', 500, 'MINIO_CONFIG_MISSING')
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectName = String(input.objectName || '').trim()
|
||||||
|
if (!objectName) {
|
||||||
|
return serviceFail('图片对象名不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewUrl = presign('GET', objectName, Number(input.expires || 3600))
|
||||||
|
return serviceOk({
|
||||||
|
objectName,
|
||||||
|
url: previewUrl,
|
||||||
|
previewUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -197,6 +197,7 @@ export const orderActionService = {
|
|||||||
return serviceFail('当前订单状态不可支付,请刷新订单状态', 400, 'ORDER_NOT_PAYABLE')
|
return serviceFail('当前订单状态不可支付,请刷新订单状态', 400, 'ORDER_NOT_PAYABLE')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const payTime = new Date()
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
await tx.supplierOrderMain.update({
|
await tx.supplierOrderMain.update({
|
||||||
where: { orderId },
|
where: { orderId },
|
||||||
@@ -206,12 +207,13 @@ export const orderActionService = {
|
|||||||
totalPrice,
|
totalPrice,
|
||||||
wPayOrAPay,
|
wPayOrAPay,
|
||||||
pointsPrice,
|
pointsPrice,
|
||||||
updateTime: new Date()
|
payTime,
|
||||||
|
updateTime: payTime
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await tx.supplierOrderSub.updateMany({
|
await tx.supplierOrderSub.updateMany({
|
||||||
where: { mainOrderId: orderId, deleted: 0 },
|
where: { mainOrderId: orderId, deleted: 0 },
|
||||||
data: { orderState: ORDER_STATE.FINISH_PAY, updateTime: new Date() }
|
data: { orderState: ORDER_STATE.FINISH_PAY, payTime, updateTime: payTime }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export const orderListService = {
|
|||||||
receiverAddress: addressView?.address || '',
|
receiverAddress: addressView?.address || '',
|
||||||
remark: subOrder.remark || mainOrder.remark || '',
|
remark: subOrder.remark || mainOrder.remark || '',
|
||||||
createTime: toDateTimeText(subOrder.createTime || subOrder.createAt),
|
createTime: toDateTimeText(subOrder.createTime || subOrder.createAt),
|
||||||
|
payTime: toDateTimeText(subOrder.payTime || mainOrder.payTime),
|
||||||
status: status.text,
|
status: status.text,
|
||||||
statusClass: status.statusClass,
|
statusClass: status.statusClass,
|
||||||
orderState: subOrder.orderState,
|
orderState: subOrder.orderState,
|
||||||
@@ -138,6 +139,7 @@ export const orderListService = {
|
|||||||
receiverAddress: addressView?.address || '',
|
receiverAddress: addressView?.address || '',
|
||||||
remark: mainOrder.remark || '',
|
remark: mainOrder.remark || '',
|
||||||
createTime: toDateTimeText(mainOrder.createTime || mainOrder.createAt),
|
createTime: toDateTimeText(mainOrder.createTime || mainOrder.createAt),
|
||||||
|
payTime: toDateTimeText(mainOrder.payTime),
|
||||||
status: mainStatus.text,
|
status: mainStatus.text,
|
||||||
statusClass: mainStatus.statusClass,
|
statusClass: mainStatus.statusClass,
|
||||||
orderState: mainOrder.orderState,
|
orderState: mainOrder.orderState,
|
||||||
|
|||||||
Reference in New Issue
Block a user