| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- <template>
- <view class="as-submit-page">
- <view v-if="pageLoading" class="as-submit-state">
- <u-loading-icon mode="circle" />
- </view>
- <template v-else-if="orderItems.length">
- <view class="as-submit-card">
- <text class="as-submit-card__title">订单商品</text>
- <text class="as-submit-card__tip">本单共 {{ orderItems.length }} 件商品,售后按整单申请</text>
- <view class="as-submit-goods">
- <order-goods-row
- v-for="item in orderItems"
- :key="item.orderItemId"
- :item="item"
- show-subtotal
- />
- </view>
- </view>
- <view class="as-submit-card">
- <text class="as-submit-card__title">申请类型</text>
- <view
- v-for="opt in applyTypeOptions"
- :key="opt.value"
- class="as-type-opt"
- :class="{ 'as-type-opt--on': applyType === opt.value }"
- @click="onApplyTypeChange(opt.value)"
- >
- {{ opt.label }}
- </view>
- </view>
- <view class="as-submit-card">
- <text class="as-submit-card__title">售后原因</text>
- <view
- v-for="reason in reasonOptions"
- :key="reason"
- class="as-reason-opt"
- :class="{ 'as-reason-opt--on': applyReason === reason }"
- @click="applyReason = reason"
- >
- {{ reason }}
- </view>
- </view>
- <view v-if="needReturnQty" class="as-submit-card">
- <text class="as-submit-card__title">退货数量</text>
- <view class="as-qty-row">
- <view class="as-qty-btn" @click="changeReturnQty(-1)">-</view>
- <text class="as-qty-num">{{ returnQuantity }}</text>
- <view class="as-qty-btn" @click="changeReturnQty(1)">+</view>
- </view>
- <text class="as-submit-card__tip">最多 {{ totalQuantity }} 件</text>
- </view>
- <view class="as-submit-card">
- <text class="as-submit-card__title">申请金额</text>
- <input
- class="as-amount-input"
- type="digit"
- v-model="applyAmountInput"
- placeholder="默认订单实付金额"
- />
- </view>
- <view class="as-submit-card">
- <text class="as-submit-card__title">补充描述</text>
- <textarea
- class="as-desc-textarea"
- v-model="description"
- :maxlength="descMax"
- placeholder="选填"
- />
- </view>
- <view class="as-submit-card">
- <image-upload-grid v-model="evidencePics" label="凭证图片" :max="evidenceMax" />
- </view>
- <button class="as-submit-btn" :disabled="submitting" @click="onSubmit">
- {{ submitting ? '提交中...' : '提交申请' }}
- </button>
- </template>
- </view>
- </template>
- <script setup>
- import { ref, computed } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import { getOrderDetail } from '@/api/order'
- import { submitAftersale } from '@/api/orderAftersale'
- import { mapOrderDetail } from '@/utils/orderDisplay'
- import { ensureApiToken } from '@/utils/apiAuth'
- import {
- ORDER_STATUS,
- AFTERSALE_APPLY_TYPE,
- AFTERSALE_APPLY_TYPE_OPTIONS,
- AFTERSALE_REASON_MAP,
- AFTERSALE_DESC_MAX,
- AFTERSALE_EVIDENCE_MAX
- } from '@/constants/order'
- /** 订单状态 → 售后申请类型(与后端 OrderConstants 一致) */
- const APPLY_TYPE_BY_ORDER_STATUS = {
- [ORDER_STATUS.PENDING_SHIP]: AFTERSALE_APPLY_TYPE.REFUND_UNSHIPPED,
- [ORDER_STATUS.SHIPPED]: AFTERSALE_APPLY_TYPE.REFUND_SHIPPED,
- [ORDER_STATUS.COMPLETED]: AFTERSALE_APPLY_TYPE.RETURN_REFUND
- }
- function resolveApplyTypeByOrderStatus(orderStatus) {
- return APPLY_TYPE_BY_ORDER_STATUS[String(orderStatus)] || ''
- }
- import { goAftersaleDetail } from '@/utils/orderNav'
- import OrderGoodsRow from '@/components/order/OrderGoodsRow.vue'
- import { useActionGuard } from '@/utils/actionGuard'
- import ImageUploadGrid from '@/components/order/ImageUploadGrid.vue'
- const submitGuard = useActionGuard()
- const orderId = ref('')
- const orderStatus = ref('')
- /** 整单商品行 */
- const orderItems = ref([])
- const payAmount = ref(0)
- const pageLoading = ref(true)
- const submitting = submitGuard.locked
- const applyType = ref('')
- const applyReason = ref('')
- const returnQuantity = ref(1)
- const applyAmountInput = ref('')
- const description = ref('')
- const evidencePics = ref([])
- /** 按订单状态只展示对应的一种申请类型 */
- const applyTypeOptions = computed(() => {
- const type = resolveApplyTypeByOrderStatus(orderStatus.value)
- if (!type) return []
- return AFTERSALE_APPLY_TYPE_OPTIONS.filter((opt) => opt.value === type)
- })
- const descMax = AFTERSALE_DESC_MAX
- const evidenceMax = AFTERSALE_EVIDENCE_MAX
- const reasonOptions = computed(() => AFTERSALE_REASON_MAP[applyType.value] || [])
- const totalQuantity = computed(() =>
- orderItems.value.reduce((sum, item) => sum + (Number(item.quantity) || 0), 0)
- )
- const needReturnQty = computed(
- () =>
- applyType.value === AFTERSALE_APPLY_TYPE.REFUND_SHIPPED ||
- applyType.value === AFTERSALE_APPLY_TYPE.RETURN_REFUND
- )
- function onApplyTypeChange(value) {
- applyType.value = value
- applyReason.value = ''
- }
- function changeReturnQty(delta) {
- const max = totalQuantity.value || 1
- const next = returnQuantity.value + delta
- returnQuantity.value = Math.max(1, Math.min(max, next))
- }
- async function loadOrder() {
- pageLoading.value = true
- try {
- const res = await getOrderDetail(orderId.value)
- const mapped = mapOrderDetail(res.data)
- const items = mapped.items || []
- if (!items.length) {
- throw new Error('订单商品不存在')
- }
- const type = resolveApplyTypeByOrderStatus(mapped.orderStatus)
- if (!type) {
- throw new Error('当前订单状态不可申请售后')
- }
- orderStatus.value = mapped.orderStatus
- applyType.value = type
- applyReason.value = ''
- orderItems.value = items
- payAmount.value = mapped.payAmount
- applyAmountInput.value = String(mapped.payAmount || '')
- returnQuantity.value = totalQuantity.value || 1
- } catch (e) {
- uni.showToast({ title: (e && e.message) || '加载失败', icon: 'none' })
- setTimeout(() => uni.navigateBack(), 800)
- } finally {
- pageLoading.value = false
- }
- }
- function onSubmit() {
- submitGuard.run(async () => {
- if (!applyReason.value) {
- uni.showToast({ title: '请选择售后原因', icon: 'none' })
- return
- }
- const amount = Number(applyAmountInput.value)
- if (!amount || amount <= 0) {
- uni.showToast({ title: '请填写申请金额', icon: 'none' })
- return
- }
- const firstItem = orderItems.value[0]
- if (!firstItem?.orderItemId) {
- uni.showToast({ title: '订单商品信息异常', icon: 'none' })
- return
- }
- const body = {
- orderId: orderId.value,
- // 接口仍要求 orderItemId;整单售后传首行 ID,金额/数量为整单口径
- orderItemId: firstItem.orderItemId,
- applyType: applyType.value,
- applyReason: applyReason.value,
- applyAmount: amount,
- description: description.value.trim(),
- evidencePics: evidencePics.value
- }
- if (needReturnQty.value) {
- body.returnQuantity = returnQuantity.value
- }
- const res = await submitAftersale(body)
- const data = res.data || {}
- uni.showToast({ title: '提交成功', icon: 'success' })
- setTimeout(() => {
- if (data.aftersaleId) {
- goAftersaleDetail(data.aftersaleId)
- } else {
- uni.navigateBack()
- }
- }, 500)
- })
- }
- onLoad((options) => {
- if (!ensureApiToken(true)) return
- orderId.value = options.orderId || ''
- if (!orderId.value) {
- uni.showToast({ title: '订单信息缺失', icon: 'none' })
- setTimeout(() => uni.navigateBack(), 800)
- return
- }
- loadOrder()
- })
- </script>
- <style lang="scss" scoped>
- .as-submit-page {
- min-height: 100vh;
- padding: 24rpx;
- padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
- background: #f5f6f8;
- box-sizing: border-box;
- }
- .as-submit-state {
- padding: 120rpx 0;
- display: flex;
- justify-content: center;
- }
- .as-submit-card {
- margin-bottom: 16rpx;
- padding: 24rpx;
- background: #fff;
- border-radius: 12rpx;
- }
- .as-submit-card__title {
- display: block;
- margin-bottom: 16rpx;
- font-size: 28rpx;
- font-weight: 600;
- color: #333;
- }
- .as-submit-card__tip {
- display: block;
- margin-bottom: 12rpx;
- font-size: 24rpx;
- color: #999;
- }
- .as-submit-goods :deep(.order-goods-row + .order-goods-row) {
- border-top: 1rpx solid #f5f5f5;
- }
- .as-type-opt,
- .as-reason-opt {
- padding: 16rpx 20rpx;
- margin-bottom: 12rpx;
- font-size: 28rpx;
- color: #333;
- background: #f9f9f9;
- border-radius: 8rpx;
- border: 2rpx solid transparent;
- }
- .as-type-opt--on,
- .as-reason-opt--on {
- border-color: #2e7d32;
- background: #f1f8f2;
- color: #2e7d32;
- }
- .as-qty-row {
- display: inline-flex;
- align-items: center;
- border: 1rpx solid #e0e0e0;
- border-radius: 8rpx;
- overflow: hidden;
- }
- .as-qty-btn {
- width: 64rpx;
- height: 64rpx;
- line-height: 64rpx;
- text-align: center;
- font-size: 32rpx;
- background: #f5f5f5;
- }
- .as-qty-num {
- min-width: 72rpx;
- text-align: center;
- font-size: 28rpx;
- }
- .as-amount-input {
- height: 72rpx;
- padding: 0 16rpx;
- font-size: 28rpx;
- background: #f9f9f9;
- border-radius: 8rpx;
- }
- .as-desc-textarea {
- width: 100%;
- min-height: 160rpx;
- padding: 16rpx;
- font-size: 28rpx;
- background: #f9f9f9;
- border-radius: 8rpx;
- box-sizing: border-box;
- }
- .as-submit-btn {
- margin-top: 16rpx;
- height: 88rpx;
- line-height: 88rpx;
- background: linear-gradient(135deg, #43a047 0%, #2e7d32 100%);
- color: #fff;
- font-size: 30rpx;
- border-radius: 44rpx;
- border: none;
- }
- .as-submit-btn[disabled] {
- opacity: 0.5;
- }
- </style>
|