Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d059443c | ||
|
|
898e239463 | ||
|
|
dcc32b3d8f | ||
|
|
fb178d304f | ||
|
|
63ff3edb11 | ||
|
|
1b45f9dac6 | ||
|
|
c443109869 | ||
|
|
53305757f8 | ||
|
|
dfe0d6bd95 | ||
|
|
88f5371ed8 |
@@ -158,3 +158,6 @@ dist
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
# points-mall
|
||||
|
||||
积分商城-供应商模块-订单操作模块
|
||||
|
||||
## 页面与接口
|
||||
|
||||
- 商家处理台:`http://localhost:3001/#/joyful-selection/merchant`
|
||||
- 供应商日志页面:`http://localhost:3001/#/joyful-selection/supplier-logs`
|
||||
- 供应商日志接口:`GET http://localhost:8888/api/supplier-logs`
|
||||
- 日志内容包含:当前后端接收请求、向zhgh发送请求、接收zhgh请求。
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "points-mall",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
VUE_APP_SUPPLIED_API_BASE="http://localhost:8888/api"
|
||||
VUE_APP_SUPPLIER_IDENTIFICATION="joyful-selection"
|
||||
@@ -0,0 +1,56 @@
|
||||
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/api'
|
||||
|
||||
const normalizeBase = base => base.replace(/\/$/, '')
|
||||
|
||||
export const getFunction = async (name, params = {}) => {
|
||||
const url = new URL(`${normalizeBase(API_BASE)}/${name}`)
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
url.searchParams.set(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
const response = await fetch(url.toString())
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
|
||||
if (!response.ok || payload.success === false) {
|
||||
const message = payload.error?.message || payload.msg || '请求失败'
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return payload.data
|
||||
}
|
||||
|
||||
export const postFunction = async (name, body = {}) => {
|
||||
const response = await fetch(`${normalizeBase(API_BASE)}/${name}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok || payload.success === false) {
|
||||
const message = payload.error?.message || payload.msg || '请求失败'
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 949 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 67 KiB |
@@ -1,11 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import Vant from 'vant'
|
||||
import { Skeleton } from 'vant'
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
||||
import 'vant/lib/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
Vue.use(Vant)
|
||||
Vue.use(Skeleton)
|
||||
Vue.use(PiniaVuePlugin)
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
||||
@@ -9,11 +9,19 @@ import JoyfulSelectionCart from '@/view/joyful-selection/cart/index.vue'
|
||||
import JoyfulSelectionCartEdit from '@/view/joyful-selection/cart/edit/index.vue'
|
||||
import JoyfulSelectionCheckout from '@/view/joyful-selection/order/checkout/index.vue'
|
||||
import JoyfulSelectionPayment from '@/view/joyful-selection/order/payment/index.vue'
|
||||
import JoyfulSelectionPaymentConfirm from '@/view/joyful-selection/order/payment-confirm/index.vue'
|
||||
import JoyfulSelectionPaymentSuccess from '@/view/joyful-selection/order/payment-success/index.vue'
|
||||
import JoyfulSelectionAddressList from '@/view/joyful-selection/address/index.vue'
|
||||
import JoyfulSelectionAddressEdit from '@/view/joyful-selection/address/edit/index.vue'
|
||||
import JoyfulSelectionOrders from '@/view/joyful-selection/profile/orders/index.vue'
|
||||
import JoyfulSelectionOrderDetail from '@/view/joyful-selection/profile/order-detail/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 JoyfulSelectionMerchantAfterSaleDetail from '@/view/joyful-selection/merchant/after-sale-detail.vue'
|
||||
import JoyfulSelectionSupplierLogs from '@/view/joyful-selection/supplier-logs/index.vue'
|
||||
import JoyfulSelectionProfile from '@/view/joyful-selection/profile/index.vue'
|
||||
import JoyfulSelectionPoints from '@/view/joyful-selection/profile/points/index.vue'
|
||||
import HomeLiving from '@/view/home-living/index.vue'
|
||||
@@ -38,11 +46,19 @@ export default new Router({
|
||||
{ path: 'cart/edit', component: JoyfulSelectionCartEdit },
|
||||
{ path: 'checkout', component: JoyfulSelectionCheckout },
|
||||
{ path: 'payment', component: JoyfulSelectionPayment },
|
||||
{ path: 'payment-confirm', component: JoyfulSelectionPaymentConfirm },
|
||||
{ path: 'payment-success', component: JoyfulSelectionPaymentSuccess },
|
||||
{ path: 'address', component: JoyfulSelectionAddressList },
|
||||
{ path: 'address/edit', component: JoyfulSelectionAddressEdit },
|
||||
{ path: 'orders', component: JoyfulSelectionOrders },
|
||||
{ path: 'order-detail', component: JoyfulSelectionOrderDetail },
|
||||
{ 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: 'merchant/after-sale-detail', component: JoyfulSelectionMerchantAfterSaleDetail },
|
||||
{ path: 'supplier-logs', component: JoyfulSelectionSupplierLogs },
|
||||
{ path: 'profile', component: JoyfulSelectionProfile },
|
||||
{ path: 'points', component: JoyfulSelectionPoints }
|
||||
]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<main class="card-skeleton">
|
||||
<div v-if="showNav" class="nav-line"></div>
|
||||
<section v-for="item in cards" :key="item" class="card-line">
|
||||
<van-skeleton title :row="rows" />
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CardSkeleton',
|
||||
props: {
|
||||
cards: {
|
||||
type: Number,
|
||||
default: 4
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
showNav: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card-skeleton {
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.nav-line {
|
||||
height: 46px;
|
||||
margin: -12px -12px 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-line {
|
||||
margin-bottom: 10px;
|
||||
padding: 16px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<main class="detail-skeleton">
|
||||
<div class="nav-line"></div>
|
||||
<div class="image-line"></div>
|
||||
<section class="card-line">
|
||||
<van-skeleton title :row="3" />
|
||||
</section>
|
||||
<section class="card-line">
|
||||
<van-skeleton title :row="2" />
|
||||
</section>
|
||||
<section class="grid-line">
|
||||
<span v-for="item in 4" :key="item"></span>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DetailSkeleton'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.detail-skeleton {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.nav-line {
|
||||
height: 54px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.image-line {
|
||||
height: 365px;
|
||||
background: #eef0f4;
|
||||
}
|
||||
|
||||
.card-line,
|
||||
.grid-line {
|
||||
margin: 10px 12px 0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-line {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.grid-line {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.grid-line span {
|
||||
height: 70px;
|
||||
border-radius: 8px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<main class="list-skeleton">
|
||||
<div v-if="showNav" class="nav-line"></div>
|
||||
<div v-if="showSearch" class="search-line"></div>
|
||||
<section v-if="showBanner" class="banner-line"></section>
|
||||
<div v-if="showTabs" class="tabs-line">
|
||||
<span v-for="item in 5" :key="item"></span>
|
||||
</div>
|
||||
<article v-for="item in rows" :key="item" class="item-line">
|
||||
<div class="thumb-line"></div>
|
||||
<div class="content-line">
|
||||
<van-skeleton title :row="3" />
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListSkeleton',
|
||||
props: {
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
showNav: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showBanner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showTabs: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.list-skeleton {
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.nav-line,
|
||||
.search-line,
|
||||
.banner-line,
|
||||
.tabs-line,
|
||||
.item-line {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.nav-line {
|
||||
height: 46px;
|
||||
margin: -12px -12px 12px;
|
||||
}
|
||||
|
||||
.search-line {
|
||||
height: 42px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 21px;
|
||||
}
|
||||
|
||||
.banner-line {
|
||||
height: 108px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tabs-line {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 44px;
|
||||
align-items: center;
|
||||
margin: -12px -12px 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.tabs-line span {
|
||||
width: 44px;
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
|
||||
.item-line {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.thumb-line {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
flex: none;
|
||||
border-radius: 6px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
|
||||
.content-line {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<main class="profile-skeleton">
|
||||
<section class="hero-line">
|
||||
<div class="avatar-line"></div>
|
||||
<van-skeleton title :row="1" />
|
||||
</section>
|
||||
<section class="asset-line">
|
||||
<div class="coin-line"></div>
|
||||
<van-skeleton title :row="1" />
|
||||
</section>
|
||||
<section v-for="item in 2" :key="item" class="panel-line">
|
||||
<van-skeleton title :row="2" />
|
||||
<div class="icon-row">
|
||||
<span v-for="icon in 5" :key="icon"></span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ProfileSkeleton'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.profile-skeleton {
|
||||
min-height: 100vh;
|
||||
padding: 18px 10px 92px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.hero-line {
|
||||
min-height: 164px;
|
||||
padding: 68px 18px 28px;
|
||||
border-radius: 0 0 16px 16px;
|
||||
background: #f2eefc;
|
||||
}
|
||||
|
||||
.avatar-line {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
float: left;
|
||||
margin-right: 12px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.asset-line,
|
||||
.panel-line {
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.asset-line {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.coin-line {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
background: #fff0e8;
|
||||
}
|
||||
|
||||
.icon-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.icon-row span {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="5" show-nav show-search show-banner />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from './components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'SupplierHomeSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="4" :rows="2" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAddressEditSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="4" show-nav />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAddressListSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="5" show-nav />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCartEditSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="4" show-nav />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCartSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="4" :show-nav="false" show-search show-banner />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionHomeSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="5" :rows="3" :show-nav="false" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionLayoutSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="5" :rows="2" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCheckoutSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="3" :rows="2" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPaymentSuccessSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="4" :rows="3" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPaymentSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<detail-skeleton />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DetailSkeleton from '../../components/DetailSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProductDetailSkeleton',
|
||||
components: { DetailSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="6" show-nav show-search />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProductListSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<profile-skeleton />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProfileSkeleton from '../../components/ProfileSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProfileSkeleton',
|
||||
components: { ProfileSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="4" :rows="3" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionOrderDetailSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="4" show-nav show-tabs />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionOrdersSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="4" :rows="2" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPointsSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="5" :rows="3" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'SupplierShopSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const UI_KEY = 'joyful-selection-ui'
|
||||
|
||||
const readUi = () => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(UI_KEY) || '{}')
|
||||
} catch (error) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const saveUi = data => {
|
||||
localStorage.setItem(UI_KEY, JSON.stringify(data))
|
||||
}
|
||||
|
||||
export const useUiStore = defineStore('joyfulSelectionUi', {
|
||||
state: () => ({
|
||||
ordersTab: 0,
|
||||
merchantTab: 'ship',
|
||||
afterSaleTab: 'processing',
|
||||
loaded: false
|
||||
}),
|
||||
actions: {
|
||||
loadUi() {
|
||||
const data = readUi()
|
||||
this.ordersTab = Number(data.ordersTab || 0)
|
||||
this.merchantTab = data.merchantTab || 'ship'
|
||||
this.afterSaleTab = data.afterSaleTab || 'processing'
|
||||
this.loaded = true
|
||||
},
|
||||
persistUi() {
|
||||
saveUi({
|
||||
ordersTab: this.ordersTab,
|
||||
merchantTab: this.merchantTab,
|
||||
afterSaleTab: this.afterSaleTab
|
||||
})
|
||||
this.loaded = true
|
||||
},
|
||||
setOrdersTab(value) {
|
||||
this.ordersTab = Number(value || 0)
|
||||
this.persistUi()
|
||||
},
|
||||
setMerchantTab(value) {
|
||||
this.merchantTab = value || 'ship'
|
||||
this.persistUi()
|
||||
},
|
||||
setAfterSaleTab(value) {
|
||||
this.afterSaleTab = value || 'processing'
|
||||
this.persistUi()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,143 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
|
||||
const USER_KEY = 'joyful-selection-user'
|
||||
const DEFAULT_SUPPLIER_IDENTIFICATION = (process.env.VUE_APP_SUPPLIER_IDENTIFICATION || '').toLowerCase()
|
||||
let bridgeTokenPromise = null
|
||||
|
||||
const readUser = () => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(USER_KEY) || '{}')
|
||||
} catch (error) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const saveUser = data => {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(data))
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||
state: () => ({
|
||||
userId: '',
|
||||
supplierIdentification: '',
|
||||
supplierId: '',
|
||||
supplierName: '',
|
||||
pointsBalance: 0,
|
||||
loaded: false,
|
||||
loadingToken: false
|
||||
}),
|
||||
getters: {
|
||||
hasToken: state => Boolean(state.supplierId && state.userId)
|
||||
},
|
||||
actions: {
|
||||
loadUser() {
|
||||
const data = readUser()
|
||||
this.userId = data.userId || ''
|
||||
const supplierIdentification = DEFAULT_SUPPLIER_IDENTIFICATION || data.supplierIdentification || ''
|
||||
const supplierChanged = Boolean(data.supplierIdentification && supplierIdentification !== data.supplierIdentification)
|
||||
|
||||
this.supplierIdentification = supplierIdentification
|
||||
this.supplierId = supplierChanged ? '' : data.supplierId || ''
|
||||
this.supplierName = supplierChanged ? '' : data.supplierName || ''
|
||||
this.pointsBalance = supplierChanged ? 0 : Number(data.pointsBalance || 0)
|
||||
this.loaded = true
|
||||
},
|
||||
persistUser() {
|
||||
saveUser({
|
||||
userId: this.userId,
|
||||
supplierIdentification: this.supplierIdentification,
|
||||
supplierId: this.supplierId,
|
||||
supplierName: this.supplierName,
|
||||
pointsBalance: this.pointsBalance
|
||||
})
|
||||
this.loaded = true
|
||||
},
|
||||
hydrateFromRoute(query = {}) {
|
||||
if (!this.loaded) {
|
||||
this.loadUser()
|
||||
}
|
||||
|
||||
const nextUserId = query.userId || ''
|
||||
|
||||
if (nextUserId && nextUserId !== this.userId) {
|
||||
this.userId = nextUserId
|
||||
this.pointsBalance = 0
|
||||
}
|
||||
if (!this.supplierIdentification) {
|
||||
this.supplierIdentification = DEFAULT_SUPPLIER_IDENTIFICATION
|
||||
}
|
||||
|
||||
this.persistUser()
|
||||
},
|
||||
async fetchSupplierInfo() {
|
||||
if (!this.loaded) {
|
||||
this.loadUser()
|
||||
}
|
||||
if (!this.supplierIdentification) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('supplier-info', {
|
||||
supplierIdentification: this.supplierIdentification
|
||||
})
|
||||
|
||||
this.supplierId = data?.supplierId || ''
|
||||
this.supplierName = data?.supplierName || ''
|
||||
this.persistUser()
|
||||
},
|
||||
async fetchBridgeToken() {
|
||||
if (!this.loaded) {
|
||||
this.loadUser()
|
||||
}
|
||||
if (!this.userId || !this.supplierIdentification || this.hasToken) {
|
||||
return
|
||||
}
|
||||
if (this.loadingToken && bridgeTokenPromise) {
|
||||
return bridgeTokenPromise
|
||||
}
|
||||
|
||||
this.loadingToken = true
|
||||
bridgeTokenPromise = (async () => {
|
||||
if (!this.supplierId) {
|
||||
await this.fetchSupplierInfo()
|
||||
}
|
||||
|
||||
const data = await postFunction('supplier-token', {
|
||||
userId: this.userId,
|
||||
supplierIdentification: this.supplierIdentification
|
||||
})
|
||||
|
||||
this.supplierId = data?.supplierId || this.supplierId
|
||||
this.supplierName = data?.supplierName || this.supplierName
|
||||
this.persistUser()
|
||||
})()
|
||||
|
||||
try {
|
||||
await bridgeTokenPromise
|
||||
} finally {
|
||||
this.loadingToken = false
|
||||
bridgeTokenPromise = null
|
||||
}
|
||||
},
|
||||
async fetchUserPoints() {
|
||||
if (!this.loaded) {
|
||||
this.loadUser()
|
||||
}
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.userId || !this.supplierId) {
|
||||
return this.pointsBalance
|
||||
}
|
||||
|
||||
const data = await postFunction('supplier-points', {
|
||||
userId: this.userId,
|
||||
supplierId: this.supplierId
|
||||
})
|
||||
|
||||
this.pointsBalance = Number(data?.userPoints || 0)
|
||||
this.persistUser()
|
||||
return this.pointsBalance
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<supplier-shop :shop="shop" />
|
||||
<div>
|
||||
<supplier-shop-skeleton v-if="loading" />
|
||||
<supplier-shop v-else :shop="shop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SupplierShop from '@/components/SupplierShop.vue'
|
||||
import SupplierShopSkeleton from '@/skeleton-view/shop/index.vue'
|
||||
import { shops } from '../shop-data'
|
||||
|
||||
export default {
|
||||
name: 'CuteSelection',
|
||||
components: { SupplierShop },
|
||||
data: () => ({ shop: shops.cuteSelection })
|
||||
components: { SupplierShop, SupplierShopSkeleton },
|
||||
data: () => ({
|
||||
loading: true,
|
||||
shop: shops.cuteSelection
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<supplier-shop :shop="shop" />
|
||||
<div>
|
||||
<supplier-shop-skeleton v-if="loading" />
|
||||
<supplier-shop v-else :shop="shop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SupplierShop from '@/components/SupplierShop.vue'
|
||||
import SupplierShopSkeleton from '@/skeleton-view/shop/index.vue'
|
||||
import { shops } from '../shop-data'
|
||||
|
||||
export default {
|
||||
name: 'FreshFruit',
|
||||
components: { SupplierShop },
|
||||
data: () => ({ shop: shops.freshFruit })
|
||||
components: { SupplierShop, SupplierShopSkeleton },
|
||||
data: () => ({
|
||||
loading: true,
|
||||
shop: shops.freshFruit
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<supplier-shop :shop="shop" />
|
||||
<div>
|
||||
<supplier-shop-skeleton v-if="loading" />
|
||||
<supplier-shop v-else :shop="shop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SupplierShop from '@/components/SupplierShop.vue'
|
||||
import SupplierShopSkeleton from '@/skeleton-view/shop/index.vue'
|
||||
import { shops } from '../shop-data'
|
||||
|
||||
export default {
|
||||
name: 'HappyShopping',
|
||||
components: { SupplierShop },
|
||||
data: () => ({ shop: shops.happyShopping })
|
||||
components: { SupplierShop, SupplierShopSkeleton },
|
||||
data: () => ({
|
||||
loading: true,
|
||||
shop: shops.happyShopping
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<supplier-shop :shop="shop" />
|
||||
<div>
|
||||
<supplier-shop-skeleton v-if="loading" />
|
||||
<supplier-shop v-else :shop="shop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SupplierShop from '@/components/SupplierShop.vue'
|
||||
import SupplierShopSkeleton from '@/skeleton-view/shop/index.vue'
|
||||
import { shops } from '../shop-data'
|
||||
|
||||
export default {
|
||||
name: 'HomeLiving',
|
||||
components: { SupplierShop },
|
||||
data: () => ({ shop: shops.homeLiving })
|
||||
components: { SupplierShop, SupplierShopSkeleton },
|
||||
data: () => ({
|
||||
loading: true,
|
||||
shop: shops.homeLiving
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<div>
|
||||
<supplier-home-skeleton v-if="loading" />
|
||||
<main v-else class="page">
|
||||
<van-nav-bar fixed placeholder title="首页" :border="false" />
|
||||
<van-search v-model="keyword" class="fixed-search" shape="round" placeholder="搜索供应商名称或商品" />
|
||||
|
||||
@@ -45,7 +47,8 @@
|
||||
<span><b>{{ service.title }}</b><small>{{ service.description }}</small></span>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -57,10 +60,15 @@ import supplierHome from '@/assets/supplier-icons/home-living.png'
|
||||
import supplierHappy from '@/assets/supplier-icons/happy-shopping.png'
|
||||
import supplierFruit from '@/assets/supplier-icons/fresh-fruit.png'
|
||||
import supplierCute from '@/assets/supplier-icons/cute-selection.png'
|
||||
import SupplierHomeSkeleton from '@/skeleton-view/index.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SupplierHomeSkeleton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
keyword: '',
|
||||
banners: [
|
||||
{ id: 'welfare', image: bannerWelfare, alt: '优质供应商精选' },
|
||||
@@ -81,6 +89,11 @@ export default {
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
filteredSuppliers() {
|
||||
const keyword = this.keyword.trim()
|
||||
|
||||
@@ -1,36 +1,7 @@
|
||||
const ADDRESS_KEY = 'joyful-selection-addresses'
|
||||
const CHOSEN_ADDRESS_KEY = 'joyful-selection-chosen-address-id'
|
||||
|
||||
const defaultAddresses = [
|
||||
{
|
||||
id: 'addr-1',
|
||||
name: '张三',
|
||||
tel: '13888888888',
|
||||
province: '浙江省',
|
||||
city: '杭州市',
|
||||
county: '余杭区',
|
||||
addressDetail: '文一西路 888 号未来科技城海创园5幢1203室',
|
||||
areaCode: '330110',
|
||||
postalCode: '310000',
|
||||
isDefault: true
|
||||
}
|
||||
]
|
||||
|
||||
export const readAddresses = () => {
|
||||
try {
|
||||
const addresses = JSON.parse(localStorage.getItem(ADDRESS_KEY) || '[]')
|
||||
return addresses.length ? addresses : defaultAddresses
|
||||
} catch (error) {
|
||||
return defaultAddresses
|
||||
}
|
||||
}
|
||||
|
||||
export const saveAddresses = addresses => {
|
||||
localStorage.setItem(ADDRESS_KEY, JSON.stringify(addresses))
|
||||
}
|
||||
|
||||
export const readChosenAddressId = () => {
|
||||
return localStorage.getItem(CHOSEN_ADDRESS_KEY) || readAddresses().find(item => item.isDefault)?.id || readAddresses()[0]?.id || ''
|
||||
return localStorage.getItem(CHOSEN_ADDRESS_KEY) || ''
|
||||
}
|
||||
|
||||
export const saveChosenAddressId = id => {
|
||||
@@ -42,7 +13,7 @@ export const formatAddress = address => {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `${address.province}${address.city}${address.county}${address.addressDetail}`
|
||||
return `${address.province || ''}${address.city || ''}${address.county || ''}${address.addressDetail || ''}`
|
||||
}
|
||||
|
||||
export const createAddressId = () => `addr-${Date.now()}`
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="address-edit-page">
|
||||
<div>
|
||||
<joyful-selection-address-edit-skeleton v-if="loading" />
|
||||
<main v-else class="address-edit-page">
|
||||
<van-nav-bar :title="pageTitle" left-arrow @click-left="$router.back()" />
|
||||
<van-address-edit
|
||||
:area-list="areaList"
|
||||
@@ -8,28 +10,39 @@
|
||||
show-delete
|
||||
show-set-default
|
||||
show-search-result
|
||||
:tel-maxlength="11"
|
||||
:search-result="searchResult"
|
||||
:area-columns-placeholder="['请选择', '请选择', '请选择']"
|
||||
@save="onSave"
|
||||
@delete="onDelete"
|
||||
@change-detail="onChangeDetail"
|
||||
/>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { Toast } from 'vant'
|
||||
import { areaList } from '@vant/area-data'
|
||||
import { createAddressId, readAddresses, saveAddresses, saveChosenAddressId } from '../address-data'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { createAddressId, saveChosenAddressId } from '../address-data'
|
||||
import JoyfulSelectionAddressEditSkeleton from '@/skeleton-view/joyful-selection/address/edit/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAddressEdit',
|
||||
components: {
|
||||
JoyfulSelectionAddressEditSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
areaList,
|
||||
searchResult: [],
|
||||
addressInfo: {}
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||
editId() {
|
||||
return this.$route.query.id
|
||||
},
|
||||
@@ -38,48 +51,65 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const address = readAddresses().find(item => item.id === this.editId)
|
||||
|
||||
if (address) {
|
||||
this.addressInfo = { ...address }
|
||||
}
|
||||
this.loadAddress()
|
||||
},
|
||||
methods: {
|
||||
onSave(content) {
|
||||
const addresses = readAddresses()
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
async loadAddress() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.editId || !this.supplierId || !this.userId) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('address-list', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId
|
||||
})
|
||||
const address = (data?.addresses || []).find(item => item.id === this.editId)
|
||||
|
||||
if (address) {
|
||||
this.addressInfo = { ...address }
|
||||
}
|
||||
} catch (error) {
|
||||
Toast(error.message || '查询地址失败,请稍后再试')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async onSave(content) {
|
||||
const nextAddress = {
|
||||
...content,
|
||||
id: this.editId || content.id || createAddressId()
|
||||
}
|
||||
const nextAddresses = addresses.filter(item => item.id !== nextAddress.id)
|
||||
|
||||
if (nextAddress.isDefault) {
|
||||
nextAddresses.forEach(item => { item.isDefault = false })
|
||||
}
|
||||
|
||||
if (!nextAddresses.length && !nextAddress.isDefault) {
|
||||
nextAddress.isDefault = true
|
||||
}
|
||||
|
||||
saveAddresses([nextAddress, ...nextAddresses])
|
||||
await this.fetchBridgeToken()
|
||||
await postFunction('address-save', {
|
||||
...nextAddress,
|
||||
addressId: nextAddress.id,
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
isDefault: ['checkout', 'after-sale'].includes(this.$route.query.from)
|
||||
})
|
||||
saveChosenAddressId(nextAddress.id)
|
||||
Toast('保存成功')
|
||||
this.$router.back()
|
||||
},
|
||||
onDelete() {
|
||||
async onDelete() {
|
||||
if (!this.editId) {
|
||||
this.$router.back()
|
||||
return
|
||||
}
|
||||
|
||||
const addresses = readAddresses().filter(item => item.id !== this.editId)
|
||||
|
||||
if (addresses.length && !addresses.some(item => item.isDefault)) {
|
||||
addresses[0].isDefault = true
|
||||
}
|
||||
|
||||
saveAddresses(addresses)
|
||||
saveChosenAddressId(addresses[0]?.id || '')
|
||||
await this.fetchBridgeToken()
|
||||
await postFunction('address-delete', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
addressId: this.editId
|
||||
})
|
||||
saveChosenAddressId('')
|
||||
Toast('删除成功')
|
||||
this.$router.back()
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="address-page">
|
||||
<div>
|
||||
<joyful-selection-address-list-skeleton v-if="loading" />
|
||||
<main v-else class="address-page">
|
||||
<van-nav-bar title="地址管理" left-arrow @click-left="$router.back()" />
|
||||
<van-address-list
|
||||
v-model="chosenAddressId"
|
||||
@@ -11,20 +13,31 @@
|
||||
@edit="onEdit"
|
||||
@select="onSelect"
|
||||
/>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatAddress, readAddresses, readChosenAddressId, saveChosenAddressId } from './address-data'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { Toast } from 'vant'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { formatAddress, readChosenAddressId, saveChosenAddressId } from './address-data'
|
||||
import JoyfulSelectionAddressListSkeleton from '@/skeleton-view/joyful-selection/address/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAddressList',
|
||||
components: {
|
||||
JoyfulSelectionAddressListSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
chosenAddressId: '',
|
||||
addresses: [],
|
||||
disabledList: []
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||
addressList() {
|
||||
return this.addresses.map(item => ({
|
||||
id: item.id,
|
||||
@@ -36,23 +49,62 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.addresses = readAddresses()
|
||||
this.chosenAddressId = readChosenAddressId()
|
||||
this.loadAddresses()
|
||||
},
|
||||
methods: {
|
||||
onSelect(item) {
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
async loadAddresses() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId) {
|
||||
Toast('用户信息不完整,请重新进入商城')
|
||||
this.addresses = []
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('address-list', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId
|
||||
})
|
||||
this.addresses = data?.addresses || []
|
||||
this.chosenAddressId = readChosenAddressId() || this.addresses.find(item => item.isDefault)?.id || this.addresses[0]?.id || ''
|
||||
} catch (error) {
|
||||
Toast(error.message || '查询地址失败,请稍后再试')
|
||||
this.addresses = []
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async onSelect(item) {
|
||||
this.chosenAddressId = 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)
|
||||
if (address) {
|
||||
try {
|
||||
await postFunction('address-save', {
|
||||
...address,
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
addressId: address.id,
|
||||
isDefault: true
|
||||
})
|
||||
} catch (error) {
|
||||
Toast(error.message || '璁剧疆榛樿鍦板潃澶辫触锛岃绋嶅悗鍐嶈瘯')
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$router.back()
|
||||
}
|
||||
},
|
||||
onAdd() {
|
||||
this.$router.push('/joyful-selection/address/edit')
|
||||
this.$router.push({ path: '/joyful-selection/address/edit', query: { from: this.$route.query.from } })
|
||||
},
|
||||
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,843 @@
|
||||
<template>
|
||||
<main class="after-sale-detail-page">
|
||||
<van-nav-bar title="售后详情" left-arrow @click-left="$router.replace('/joyful-selection/after-sale')" />
|
||||
|
||||
<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 class="service-no-row"><span>售后单号</span><b class="scrollable-no">{{ afterSale.serviceOrderId || '-' }}</b><button type="button" @click="copyServiceNo">复制</button></p>
|
||||
<p><span>子订单号</span><b>{{ afterSale.orderNumberId || afterSale.subOrderId || '-' }}</b></p>
|
||||
<p><span>售后类型</span><b>{{ afterSale.serviceTypeText || '-' }}</b></p>
|
||||
<p><span>售后原因</span><b>{{ afterSale.reason || '-' }}</b></p>
|
||||
<p><span>问题描述</span><b>{{ afterSale.problemDescription || '-' }}</b></p>
|
||||
<p><span>申请时间</span><b>{{ afterSale.applyTime || '-' }}</b></p>
|
||||
<p v-if="!isUnsignedAfterSale"><span>取件方式</span><b>{{ pickupMethodText }}</b></p>
|
||||
<p v-if="!isUnsignedAfterSale"><span>取件地址</span><b>{{ pickupAddressText }}</b></p>
|
||||
<div v-if="images.length" class="voucher-block">
|
||||
<h4>上传凭证</h4>
|
||||
<div>
|
||||
<van-image v-for="image in images" :key="image.url" :src="image.url" fit="cover" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer v-show="!loading" class="bottom-actions">
|
||||
<button v-if="canCancel" type="button">撤销申请</button>
|
||||
<button v-if="needUserShipNo" type="button" class="primary" @click="fillLogisticsNo">填写物流单号</button>
|
||||
<button v-else-if="canConfirmExchangeReceive" type="button" class="primary" @click="confirmExchangeReceive">确认收货</button>
|
||||
<button v-else type="button" class="primary" @click="contactService">联系客服</button>
|
||||
</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
|
||||
},
|
||||
isUnsignedAfterSale() {
|
||||
if (this.afterSale.isSigned === false || this.afterSale.isSigned === 0 || this.afterSale.isSigned === 'false') {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.afterSale.isSigned === undefined &&
|
||||
Number(this.afterSale.logisticsState) === LOGISTICS_STATE.NONE &&
|
||||
!this.afterSale.pickupMethod &&
|
||||
!this.afterSale.pickupName &&
|
||||
!this.afterSale.pickupMobile &&
|
||||
!this.afterSale.pickupAddress
|
||||
},
|
||||
statusCardClass() {
|
||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.REJECTED) return 'rejected'
|
||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.APPROVED) return 'approved'
|
||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.FINISHED) return 'finished'
|
||||
return 'pending'
|
||||
},
|
||||
statusIcon() {
|
||||
if (this.statusCardClass === 'approved' || this.statusCardClass === 'finished') return 'success'
|
||||
if (this.statusCardClass === 'rejected') return 'cross'
|
||||
return 'clock-o'
|
||||
},
|
||||
statusTitle() {
|
||||
const serviceState = Number(this.afterSale.serviceState)
|
||||
const logisticsState = Number(this.afterSale.logisticsState)
|
||||
if (serviceState === SERVICE_STATE.REJECTED) return '审核未通过'
|
||||
if (serviceState === SERVICE_STATE.FINISHED) return '售后完成'
|
||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已发货'
|
||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '商家待发货'
|
||||
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '商品寄回中'
|
||||
if (serviceState === SERVICE_STATE.APPROVED) return '审核通过'
|
||||
return '审核中'
|
||||
},
|
||||
statusDesc() {
|
||||
const serviceState = Number(this.afterSale.serviceState)
|
||||
const logisticsState = Number(this.afterSale.logisticsState)
|
||||
if (serviceState === SERVICE_STATE.REJECTED) return '商家未通过您的售后申请,请查看审核意见'
|
||||
if (serviceState === SERVICE_STATE.FINISHED) return '售后流程已完成'
|
||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已发出换货商品,请确认收货'
|
||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '商家已收到商品,等待发出换货商品'
|
||||
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '您已寄回商品,等待商家收货'
|
||||
if (this.isExchange && this.isUnsignedAfterSale && serviceState === SERVICE_STATE.APPROVED) return '商家已通过您的申请,等待商家发货'
|
||||
if (serviceState === SERVICE_STATE.APPROVED) return '商家已通过您的申请,请按要求寄回商品'
|
||||
return '商家正在审核您的售后申请,请耐心等待'
|
||||
},
|
||||
progressSteps() {
|
||||
const serviceState = Number(this.afterSale.serviceState)
|
||||
const logisticsState = Number(this.afterSale.logisticsState)
|
||||
const steps = [
|
||||
{
|
||||
key: 'apply',
|
||||
title: '提交申请',
|
||||
time: this.afterSale.applyTime,
|
||||
desc: '您已成功提交售后申请',
|
||||
state: 'done'
|
||||
},
|
||||
{
|
||||
key: 'audit',
|
||||
title: '商家审核',
|
||||
time: this.afterSale.auditTime,
|
||||
desc: serviceState === SERVICE_STATE.REJECTED ? '商家审核未通过' : serviceState >= SERVICE_STATE.APPROVED ? '商家已审核通过' : '商家正在审核中',
|
||||
state: serviceState >= SERVICE_STATE.APPROVED || serviceState === SERVICE_STATE.REJECTED ? 'done' : 'current'
|
||||
},
|
||||
{
|
||||
key: 'userShip',
|
||||
title: '寄回商品',
|
||||
time: this.afterSale.userShipTime,
|
||||
desc: logisticsState >= LOGISTICS_STATE.USER_SHIPPED ? '您已寄回商品' : '请在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.isUnsignedAfterSale && !this.isExchange) {
|
||||
steps.splice(2)
|
||||
steps.push({
|
||||
key: 'finish',
|
||||
title: '售后完成',
|
||||
time: this.afterSale.finishTime,
|
||||
desc: serviceState === SERVICE_STATE.FINISHED ? '商家同意,售后完成' : '商家同意后售后完成',
|
||||
state: serviceState === SERVICE_STATE.FINISHED ? 'done' : serviceState >= SERVICE_STATE.APPROVED ? 'current' : 'todo'
|
||||
})
|
||||
|
||||
return steps.map((step, index) => ({
|
||||
...step,
|
||||
state: serviceState === SERVICE_STATE.REJECTED && index > 1 ? 'todo' : step.state,
|
||||
subTitle: step.state === 'done' ? '' : '-'
|
||||
}))
|
||||
}
|
||||
|
||||
if (this.isExchange) {
|
||||
if (this.isUnsignedAfterSale) {
|
||||
steps.splice(2)
|
||||
}
|
||||
|
||||
steps.push(
|
||||
{
|
||||
key: 'merchantShip',
|
||||
title: '商家发货',
|
||||
time: this.afterSale.merchantShipTime,
|
||||
desc: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? '商家已发出换货商品' : '等待商家发出换货商品',
|
||||
state: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? 'done' : (this.isUnsignedAfterSale ? serviceState >= SERVICE_STATE.APPROVED : logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED) ? 'current' : 'todo'
|
||||
},
|
||||
{
|
||||
key: 'userReceive',
|
||||
title: '用户收货',
|
||||
time: this.afterSale.finishTime,
|
||||
desc: logisticsState >= LOGISTICS_STATE.USER_RECEIVED ? '您已收到换货商品' : '等待用户收货',
|
||||
state: logisticsState >= LOGISTICS_STATE.USER_RECEIVED ? 'done' : logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED ? 'current' : 'todo'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
steps.push({
|
||||
key: 'finish',
|
||||
title: '售后完成',
|
||||
time: this.afterSale.finishTime,
|
||||
desc: serviceState === SERVICE_STATE.FINISHED ? '售后流程已完成' : '售后流程未完成',
|
||||
state: serviceState === SERVICE_STATE.FINISHED ? 'done' : !this.isExchange && logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED ? 'current' : 'todo'
|
||||
})
|
||||
|
||||
return 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 && !this.isUnsignedAfterSale
|
||||
},
|
||||
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
|
||||
},
|
||||
canConfirmExchangeReceive() {
|
||||
return this.isExchange && Number(this.afterSale.logisticsState) === LOGISTICS_STATE.MERCHANT_SHIPPED
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
async confirmExchangeReceive() {
|
||||
await Dialog.confirm({
|
||||
title: '确认收到换货商品?',
|
||||
message: '确认后该换货售后将完成。'
|
||||
})
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
await postFunction('after-sale-logistics', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
serviceOrderId: this.afterSale.serviceOrderId,
|
||||
logisticsState: LOGISTICS_STATE.USER_RECEIVED
|
||||
})
|
||||
Toast('已确认收货')
|
||||
await this.loadDetail()
|
||||
} catch (error) {
|
||||
Toast(error.message || '确认收货失败,请稍后再试')
|
||||
}
|
||||
},
|
||||
contactService() {
|
||||
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;
|
||||
}
|
||||
|
||||
.info-card .service-no-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.address-card b,
|
||||
.info-card b {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-weight: 400;
|
||||
text-align: right;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.info-card .scrollable-no {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
overflow-wrap: normal;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.info-card .scrollable-no::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info-card button {
|
||||
flex: none;
|
||||
align-self: center;
|
||||
height: 24px;
|
||||
margin-left: 8px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: #ff5b20;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.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,964 @@
|
||||
<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="form-card">
|
||||
<div class="method-row">
|
||||
<span>是否签收</span>
|
||||
<button type="button" :class="{ active: isSigned === false }" @click="isSigned = false">未签收</button>
|
||||
<button type="button" :class="{ active: isSigned === true }" @click="isSigned = true">已签收</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-show="!loading && isSigned" class="form-card">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section v-show="!loading" class="exchange-card">
|
||||
<div class="exchange-head method-row">
|
||||
<h3>换货商品</h3>
|
||||
</div>
|
||||
<div class="exchange-body">
|
||||
<article v-if="selectedExchangeGoods.sku" class="product-row">
|
||||
<van-image class="product-thumb" :src="selectedExchangeGoods.image" fit="cover" />
|
||||
<div class="product-info">
|
||||
<h4>{{ selectedExchangeGoods.name || '-' }}</h4>
|
||||
<p>{{ skuText(selectedExchangeGoods) }}</p>
|
||||
<b>¥{{ selectedExchangeGoods.price || '0.00' }}</b>
|
||||
</div>
|
||||
<span>x1</span>
|
||||
</article>
|
||||
<p v-else>请选择与原商品价格一致的换货商品</p>
|
||||
<button type="button" @click="openExchangeGoodsPopup">{{ selectedExchangeGoods.sku ? '重新选择' : '选择商品' }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-show="!loading" class="form-card">
|
||||
<div class="form-row reason-row" @click="showReasonPopup = true">
|
||||
<span>售后原因</span>
|
||||
<div class="reason-value">
|
||||
<b>{{ selectedReasonText }}</b>
|
||||
<van-icon name="arrow" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="desc-block">
|
||||
<h3>问题描述</h3>
|
||||
<textarea v-model="description" maxlength="200" placeholder="请详细描述遇到的问题(选填)"></textarea>
|
||||
<em>{{ description.length }}/200</em>
|
||||
</div>
|
||||
|
||||
<div class="upload-block">
|
||||
<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" :style="{ height: '80vh' }">
|
||||
<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 ORDER_STATE = {
|
||||
RECEIVED: 4,
|
||||
COMPLETED: 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,
|
||||
isSigned: 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
|
||||
.map(item => this.normalizeExchangeGoods(item))
|
||||
.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 || '请选择售后原因'
|
||||
},
|
||||
hasProductShipped() {
|
||||
// Assuming currentSubOrder.logisticsState indicates if the product has been shipped to the user.
|
||||
// If the product is not in 'NONE' (0) or 'WAIT_USER_SHIP' (1) state, it's considered shipped.
|
||||
const logisticsState = Number(this.currentSubOrder.logisticsState);
|
||||
return logisticsState !== 0 && logisticsState !== 1;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadOrderDetail()
|
||||
this.loadPickupAddress()
|
||||
},
|
||||
activated() {
|
||||
this.loadOrderDetail()
|
||||
this.loadPickupAddress()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
skuText(item = {}) {
|
||||
if (item.skuText) {
|
||||
return item.skuText
|
||||
}
|
||||
|
||||
const sku = String(item.sku || '').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(' | ') : '-'
|
||||
},
|
||||
normalizeExchangeGoods(item = {}) {
|
||||
const color = 'cream'
|
||||
const size = 'standard'
|
||||
const sku = item.sku || `${item.id}-${color}-${size}`
|
||||
const skuText = [SKU_COLOR_MAP[color] || color, SKU_SIZE_MAP[size] || size].filter(Boolean).join(' | ')
|
||||
|
||||
return {
|
||||
...item,
|
||||
sku,
|
||||
skuText
|
||||
}
|
||||
},
|
||||
selectReason(item) {
|
||||
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
|
||||
},
|
||||
inferIsSigned(order = {}, subOrder = {}) {
|
||||
const state = Number(subOrder.orderState ?? order.orderState)
|
||||
|
||||
return [ORDER_STATE.RECEIVED, ORDER_STATE.COMPLETED].includes(state)
|
||||
},
|
||||
checkSignedMatch() {
|
||||
const realSigned = this.inferIsSigned(this.currentOrder, this.currentSubOrder)
|
||||
if (this.isSigned !== realSigned) {
|
||||
Toast(realSigned ? '该订单已签收,请选择已签收' : '该订单未签收,请选择未签收')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
async afterRead(fileItems) {
|
||||
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 || {}
|
||||
this.isSigned = this.inferIsSigned(this.currentOrder, this.currentSubOrder)
|
||||
} 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.checkSignedMatch()) {
|
||||
return
|
||||
}
|
||||
if (!this.selectedExchangeGoods.sku) {
|
||||
Toast('请选择换货商品')
|
||||
return
|
||||
}
|
||||
|
||||
// 售后原因必填
|
||||
if (!this.selectedReason || this.selectedReason === '') {
|
||||
Toast('请选择售后原因');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已签收,且取件方式是上门取件,则取件信息必填
|
||||
if (this.isSigned && this.pickupMethod === 'door') {
|
||||
if (!this.receiverNameText || this.receiverNameText === '-' ||
|
||||
!this.receiverMobileText || this.receiverMobileText === '-' ||
|
||||
!this.receiverAddressText || this.receiverAddressText === '-') {
|
||||
Toast('请填写完整的取件信息');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const applied = await postFunction('after-sale-apply', {
|
||||
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.isSigned ? this.pickupMethod : undefined, // Conditionally set
|
||||
pickupName: this.isSigned && this.receiverNameText !== '-' ? this.receiverNameText : undefined, // Conditionally set
|
||||
pickupMobile: this.isSigned && this.receiverMobileText !== '-' ? this.receiverMobileText : undefined, // Conditionally set
|
||||
pickupAddress: this.isSigned && this.receiverAddressText !== '-' ? this.receiverAddressText : undefined, // Conditionally set
|
||||
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: 0 12px 5px;
|
||||
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: flex-start;
|
||||
gap: 8px;
|
||||
min-height: 42px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.exchange-head h3 {
|
||||
margin-bottom: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.exchange-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.exchange-body button {
|
||||
align-self: center;
|
||||
flex: none;
|
||||
min-width: 92px;
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid #ff5b20;
|
||||
border-radius: 6px;
|
||||
color: #ff5b20;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
background: #fff7f2;
|
||||
}
|
||||
|
||||
.exchange-body > p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.exchange-card .product-row {
|
||||
flex: 1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.exchange-card .product-info {
|
||||
padding-left: 8px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.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;
|
||||
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 {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 16px 12px 12px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.exchange-popup h3 {
|
||||
margin: 0 0 6px;
|
||||
color: #222;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.exchange-popup > p {
|
||||
flex: none;
|
||||
margin: 0 0 12px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.exchange-goods-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.exchange-goods-item {
|
||||
display: flex;
|
||||
min-height: 116px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px 10px 10px;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 18px rgba(0, 0, 0, .045);
|
||||
}
|
||||
|
||||
.exchange-goods-item + .exchange-goods-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.exchange-goods-thumb {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
min-width: 88px;
|
||||
max-width: 88px;
|
||||
min-height: 88px;
|
||||
max-height: 88px;
|
||||
aspect-ratio: 1 / 1;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.exchange-goods-thumb ::v-deep .van-image__img,
|
||||
.exchange-goods-thumb ::v-deep img {
|
||||
display: block;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.exchange-goods-item > div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.exchange-goods-item h4 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
color: #111;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.exchange-goods-item p {
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin: 6px 0 0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: #8f8f8f;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
background: #f5f6f8;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.exchange-goods-item b {
|
||||
color: #ff5b20;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.exchange-goods-item > .van-icon {
|
||||
flex: none;
|
||||
color: #ff5b20;
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<main class="after-sale-page">
|
||||
<van-nav-bar title="退款/售后" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
|
||||
|
||||
<van-pull-refresh v-model="refreshing" class="after-sale-list" @refresh="onRefresh">
|
||||
<van-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>
|
||||
<article v-for="item in currentAfterSaleItems" :key="item.serviceOrderId" class="after-sale-card" @click="goDetail(item)">
|
||||
<header class="after-sale-card__header">
|
||||
<span>{{ item.serviceTypeText }}</span>
|
||||
<b>{{ item.serviceStateText }}</b>
|
||||
</header>
|
||||
<span class="signed-stamp" :class="{ unsigned: item.isSigned === false }">{{ item.isSigned === false ? '未签收' : '已签收' }}</span>
|
||||
|
||||
<div class="after-sale-card__body">
|
||||
<div
|
||||
v-for="product in previewProducts(item.products)"
|
||||
:key="product.id"
|
||||
class="after-sale-product"
|
||||
>
|
||||
<van-image class="after-sale-thumb" :src="product.image" fit="cover" />
|
||||
<div class="after-sale-product__info">
|
||||
<h3>{{ product.name || '-' }}</h3>
|
||||
<p>{{ skuText(product) }}</p>
|
||||
<b>¥{{ product.price || item.amount }}</b>
|
||||
</div>
|
||||
<span>x{{ product.number || 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="after-sale-meta">
|
||||
<p><span>售后单号</span><b>{{ item.serviceOrderId }}</b></p>
|
||||
<p><span>子订单号</span><b>{{ item.orderNumberId || item.subOrderId || '-' }}</b></p>
|
||||
<p><span>申请时间</span><b>{{ item.applyTime || '-' }}</b></p>
|
||||
<p><span>物流状态</span><b>{{ item.logisticsStateText }}</b></p>
|
||||
<p v-if="item.reason"><span>申请原因</span><b>{{ item.reason }}</b></p>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</section>
|
||||
</van-pull-refresh>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { Toast } from 'vant'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useUiStore } from '@/store/ui'
|
||||
|
||||
const SKU_COLOR_MAP = {
|
||||
cream: '奶油白',
|
||||
blue: '雾霾蓝',
|
||||
orange: '暖橙色'
|
||||
}
|
||||
|
||||
const SKU_SIZE_MAP = {
|
||||
standard: '标准款',
|
||||
plus: '升级款'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAfterSale',
|
||||
data: () => ({
|
||||
afterSaleItems: [],
|
||||
loading: false,
|
||||
refreshing: false
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||
...mapState(useUiStore, ['afterSaleTab']),
|
||||
activeTab: {
|
||||
get() {
|
||||
return this.afterSaleTab
|
||||
},
|
||||
set(value) {
|
||||
this.setAfterSaleTab(value)
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.loadUi()
|
||||
this.loadAfterSales()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
...mapActions(useUiStore, ['loadUi', 'setAfterSaleTab']),
|
||||
previewProducts(products = []) {
|
||||
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) {
|
||||
if (showLoading) {
|
||||
this.loading = true
|
||||
}
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId) {
|
||||
Toast('用户信息不完整,请重新进入商城')
|
||||
this.afterSaleItems = []
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('after-sale-list', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId
|
||||
})
|
||||
|
||||
this.afterSaleItems = data?.afterSaleItems || []
|
||||
} catch (error) {
|
||||
Toast(error.message || '查询退款/售后失败,请稍后再试')
|
||||
this.afterSaleItems = []
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
async onRefresh() {
|
||||
try {
|
||||
await this.loadAfterSales(false)
|
||||
} finally {
|
||||
this.refreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.after-sale-page {
|
||||
min-height: 100vh;
|
||||
padding-top: 46px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.after-sale-page ::v-deep .van-nav-bar {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.after-sale-list {
|
||||
min-height: calc(100vh - 46px);
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.after-sale-tab-panel {
|
||||
padding: 12px 10px 0;
|
||||
}
|
||||
|
||||
.after-sale-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.signed-stamp {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 15px;
|
||||
bottom: 25px;
|
||||
display: flex;
|
||||
width: 95px;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid rgba(255, 91, 32, .8);
|
||||
border-radius: 50%;
|
||||
color: #ff5b20;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
opacity: .9;
|
||||
background: rgba(255, 241, 232, .55);
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.signed-stamp::before {
|
||||
position: absolute;
|
||||
inset: 4px;
|
||||
border: 1px dashed currentColor;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.signed-stamp.unsigned {
|
||||
border-color: rgba(153, 153, 153, .9);
|
||||
color: #666;
|
||||
background: rgba(246, 247, 249, .7);
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
padding: 14px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.skeleton-card ::v-deep .van-skeleton + .van-skeleton {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.after-sale-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 14px 8px;
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.after-sale-card__header b {
|
||||
color: #ff5b20;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.after-sale-card__header span {
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
color: #ff5b20;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
background: #fff0e9;
|
||||
}
|
||||
|
||||
.after-sale-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.after-sale-product {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.after-sale-thumb {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.after-sale-product__info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.after-sale-product__info h3 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0 0 6px;
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.after-sale-product__info p {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.after-sale-product__info b {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
color: #ff5b20;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.after-sale-product > span {
|
||||
flex: none;
|
||||
align-self: flex-end;
|
||||
color: #222;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.after-sale-meta {
|
||||
padding: 0 14px 14px;
|
||||
}
|
||||
|
||||
.after-sale-meta p {
|
||||
display: flex;
|
||||
margin: 8px 0 0;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.after-sale-meta span {
|
||||
flex: none;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.after-sale-meta b {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
color: #555;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,702 @@
|
||||
<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="method-row">
|
||||
<span>是否签收</span>
|
||||
<button type="button" :class="{ active: isSigned === false }" @click="isSigned = false">未签收</button>
|
||||
<button type="button" :class="{ active: isSigned === true }" @click="isSigned = true">已签收</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-show="!loading && isSigned" class="form-card">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section v-show="!loading" class="form-card">
|
||||
<div class="form-row reason-row" @click="showReasonPopup = true">
|
||||
<span>售后原因</span>
|
||||
<div class="reason-value">
|
||||
<b>{{ selectedReasonText }}</b>
|
||||
<van-icon name="arrow" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="desc-block">
|
||||
<h3>问题描述</h3>
|
||||
<textarea v-model="description" maxlength="200" placeholder="请详细描述遇到的问题(选填)"></textarea>
|
||||
<em>{{ description.length }}/200</em>
|
||||
</div>
|
||||
|
||||
<div class="upload-block">
|
||||
<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 ORDER_STATE = {
|
||||
RECEIVED: 4,
|
||||
COMPLETED: 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: '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,
|
||||
isSigned: 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 || '请选择售后原因'
|
||||
},
|
||||
hasProductShipped() {
|
||||
// Assuming currentSubOrder.logisticsState indicates if the product has been shipped to the user.
|
||||
// If the product is not in 'NONE' (0) or 'WAIT_USER_SHIP' (1) state, it's considered shipped.
|
||||
const logisticsState = Number(this.currentSubOrder.logisticsState);
|
||||
return logisticsState !== 0 && logisticsState !== 1;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadOrderDetail()
|
||||
this.loadPickupAddress()
|
||||
},
|
||||
activated() {
|
||||
this.loadOrderDetail()
|
||||
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
|
||||
},
|
||||
inferIsSigned(order = {}, subOrder = {}) {
|
||||
const state = Number(subOrder.orderState ?? order.orderState)
|
||||
|
||||
return [ORDER_STATE.RECEIVED, ORDER_STATE.COMPLETED].includes(state)
|
||||
},
|
||||
checkSignedMatch() {
|
||||
const realSigned = this.inferIsSigned(this.currentOrder, this.currentSubOrder)
|
||||
if (this.isSigned !== realSigned) {
|
||||
Toast(realSigned ? '该订单已签收,请选择已签收' : '该订单未签收,请选择未签收')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
async afterRead(fileItems) {
|
||||
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 || {}
|
||||
this.isSigned = this.inferIsSigned(this.currentOrder, this.currentSubOrder)
|
||||
} 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.checkSignedMatch()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 售后原因必填
|
||||
if (!this.selectedReason || this.selectedReason === '') {
|
||||
Toast('请选择售后原因');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已签收,且取件方式是上门取件,则取件信息必填
|
||||
if (this.isSigned && this.pickupMethod === 'door') {
|
||||
if (!this.receiverNameText || this.receiverNameText === '-' ||
|
||||
!this.receiverMobileText || this.receiverMobileText === '-' ||
|
||||
!this.receiverAddressText || this.receiverAddressText === '-') {
|
||||
Toast('请填写完整的取件信息');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const applied = await postFunction('after-sale-apply', {
|
||||
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.isSigned ? this.pickupMethod : undefined, // Conditionally set
|
||||
pickupName: this.isSigned && this.receiverNameText !== '-' ? this.receiverNameText : undefined, // Conditionally set
|
||||
pickupMobile: this.isSigned && this.receiverMobileText !== '-' ? this.receiverMobileText : undefined, // Conditionally set
|
||||
pickupAddress: this.isSigned && this.receiverAddressText !== '-' ? this.receiverAddressText : undefined, // Conditionally set
|
||||
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: 0 12px 5px;
|
||||
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;
|
||||
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>
|
||||
@@ -2,7 +2,15 @@
|
||||
<section class="cart-panel" :class="{ 'cart-panel--page': pageMode }">
|
||||
<van-notice-bar v-if="showNotice" text="满 199 元包邮,快去凑单吧~" left-icon="info-o" />
|
||||
<section class="cart-list">
|
||||
<van-empty v-if="!items.length" description="购物车还是空的" />
|
||||
<div v-if="!items.length" class="cart-empty">
|
||||
<van-empty description="购物车还是空的" />
|
||||
<van-button
|
||||
class="cart-empty__button"
|
||||
round
|
||||
type="danger"
|
||||
@click="$router.push('/joyful-selection/products')"
|
||||
>去购物</van-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<article
|
||||
v-for="item in items"
|
||||
@@ -37,10 +45,11 @@
|
||||
</template>
|
||||
</section>
|
||||
<van-submit-bar
|
||||
v-if="items.length"
|
||||
:fixed="fixedSubmit"
|
||||
:price="selectedTotalPrice"
|
||||
:button-text="submitText"
|
||||
@submit="$emit('checkout', selectedItems)"
|
||||
@submit="handleCheckout"
|
||||
>
|
||||
<van-checkbox
|
||||
:value="isAllChecked"
|
||||
@@ -139,6 +148,14 @@ export default {
|
||||
}
|
||||
|
||||
this.persistCart()
|
||||
},
|
||||
handleCheckout() {
|
||||
if (!this.items.length || !this.selectedItems.length) {
|
||||
this.$toast('请选择要结算的商品')
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('checkout', this.selectedItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +190,19 @@ export default {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cart-empty {
|
||||
padding-top: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cart-empty__button {
|
||||
width: 120px;
|
||||
height: 38px;
|
||||
background: #ff5b20;
|
||||
border-color: #ff5b20;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
display: flex;
|
||||
min-height: 128px;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="edit-page">
|
||||
<div>
|
||||
<joyful-selection-cart-edit-skeleton v-if="loading" />
|
||||
<main v-else class="edit-page">
|
||||
<van-nav-bar title="购物车" right-text="完成" @click-right="$router.push('/joyful-selection/cart')" />
|
||||
<section class="edit-list">
|
||||
<van-empty v-if="!items.length" description="购物车还是空的" />
|
||||
@@ -30,17 +32,23 @@
|
||||
@input="toggleAll"
|
||||
>全选</van-checkbox>
|
||||
</van-submit-bar>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Toast } from 'vant'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import JoyfulSelectionCartEditSkeleton from '@/skeleton-view/joyful-selection/cart/edit/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCartEdit',
|
||||
components: {
|
||||
JoyfulSelectionCartEditSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
selectedIds: []
|
||||
}),
|
||||
computed: {
|
||||
@@ -55,6 +63,9 @@ export default {
|
||||
created() {
|
||||
this.loadCart()
|
||||
this.selectedIds = this.items.map(item => item.cartId)
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCartStore, ['loadCart', 'removeItems']),
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
<template>
|
||||
<main class="cart-page">
|
||||
<div>
|
||||
<joyful-selection-cart-skeleton v-if="loading" />
|
||||
<main v-else class="cart-page">
|
||||
<van-nav-bar title="购物车" right-text="编辑" @click-right="$router.push('/joyful-selection/cart/edit')" />
|
||||
<joyful-cart-panel fixed-submit page-mode @checkout="goCheckout" />
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JoyfulCartPanel from './components/JoyfulCartPanel.vue'
|
||||
import JoyfulSelectionCartSkeleton from '@/skeleton-view/joyful-selection/cart/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCart',
|
||||
components: {
|
||||
JoyfulCartPanel
|
||||
JoyfulCartPanel,
|
||||
JoyfulSelectionCartSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
goCheckout(items) {
|
||||
if (!items || !items.length) {
|
||||
this.$toast('请选择要结算的商品')
|
||||
return
|
||||
}
|
||||
|
||||
sessionStorage.setItem('joyful-selection-checkout-items', JSON.stringify(items))
|
||||
this.$router.push({ path: '/joyful-selection/checkout', query: { source: 'cart' } })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="joy-home">
|
||||
<div>
|
||||
<joyful-selection-home-skeleton v-if="loading" />
|
||||
<main v-else class="joy-home">
|
||||
<van-search class="fixed-search" v-model="keyword" shape="round" placeholder="搜索商品名称" />
|
||||
|
||||
<section class="hero">
|
||||
@@ -24,7 +26,7 @@
|
||||
<section class="product-section">
|
||||
<header class="section-head"><b>限时秒杀</b><a @click="$router.push('/joyful-selection/products')">更多 ›</a></header>
|
||||
<div class="product-row">
|
||||
<article v-for="item in flashProducts" :key="item.name" @click="goProductDetail(item)">
|
||||
<article v-for="item in flashProducts" :key="item.id" @click="goProductDetail(item)">
|
||||
<van-image :src="item.image" fit="cover" />
|
||||
<div class="product-meta">
|
||||
<p>{{ item.name }}</p><b>¥{{ item.price }}</b>
|
||||
@@ -36,7 +38,7 @@
|
||||
<section class="product-section hot-section">
|
||||
<header class="section-head"><b>爆款推荐</b><a @click="$router.push('/joyful-selection/products')">更多 ›</a></header>
|
||||
<div class="product-row">
|
||||
<article v-for="item in hotProducts" :key="item.name" @click="goProductDetail(item)">
|
||||
<article v-for="item in hotProducts" :key="item.id" @click="goProductDetail(item)">
|
||||
<van-image :src="item.image" fit="cover" />
|
||||
<div class="product-meta">
|
||||
<p>{{ item.name }}</p><b>¥{{ item.price }}</b>
|
||||
@@ -44,17 +46,22 @@
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { joyfulProducts } from './product-data'
|
||||
import { getFunction } from '@/api/supplied-service'
|
||||
import JoyfulSelectionHomeSkeleton from '@/skeleton-view/joyful-selection/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionHome',
|
||||
components: {
|
||||
JoyfulSelectionHomeSkeleton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
keyword: '',
|
||||
categories: [
|
||||
{ name: '精选推荐', icon: 'like', color: '#ff6e8a', background: '#fff0f3' },
|
||||
@@ -68,11 +75,33 @@ export default {
|
||||
{ name: '超值专区', icon: 'label', color: '#ec8c3d', background: '#fff4e9' },
|
||||
{ name: '全部分类', icon: 'apps-o', color: '#989898', background: '#f5f5f5' }
|
||||
],
|
||||
flashProducts: joyfulProducts.slice(0, 4),
|
||||
hotProducts: [joyfulProducts[2], joyfulProducts[0], joyfulProducts[1], joyfulProducts[3]]
|
||||
products: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
flashProducts() {
|
||||
return this.products.slice(0, 4)
|
||||
},
|
||||
hotProducts() {
|
||||
return this.products.slice(0, 4).reverse()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadProducts()
|
||||
},
|
||||
methods: {
|
||||
async loadProducts() {
|
||||
this.loading = true
|
||||
try {
|
||||
const data = await getFunction('product-list')
|
||||
this.products = data?.products || []
|
||||
} catch (error) {
|
||||
console.warn('查询商品列表失败', error)
|
||||
this.$toast(error.message || '查询商品列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
goProductDetail(item) {
|
||||
this.$router.push({ path: '/joyful-selection/product-detail', query: { id: item.id } })
|
||||
}
|
||||
@@ -199,8 +228,8 @@ export default {
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 18px;
|
||||
align-items: center;
|
||||
margin: 0 0 8px 1px;
|
||||
}
|
||||
|
||||
@@ -229,10 +258,10 @@ export default {
|
||||
|
||||
.product-row .van-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
@@ -269,5 +298,4 @@ export default {
|
||||
.hot-section {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<template>
|
||||
<main class="joyful-layout">
|
||||
<router-view />
|
||||
<joyful-selection-layout-skeleton v-if="sessionLoading" />
|
||||
<router-view v-else />
|
||||
<van-tabbar v-if="showTabbar" route active-color="#ff5b20" inactive-color="#777">
|
||||
<van-tabbar-item icon="wap-home-o" to="/joyful-selection/home">首页</van-tabbar-item>
|
||||
<van-tabbar-item icon="apps-o" to="/joyful-selection/products">分类</van-tabbar-item>
|
||||
@@ -13,9 +14,17 @@
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import JoyfulSelectionLayoutSkeleton from '@/skeleton-view/joyful-selection/layout.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionLayout',
|
||||
components: {
|
||||
JoyfulSelectionLayoutSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
sessionLoading: true
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useCartStore, ['totalCount']),
|
||||
cartBadge() {
|
||||
@@ -27,9 +36,29 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.loadCart()
|
||||
this.prepareUserSession()
|
||||
},
|
||||
watch: {
|
||||
'$route.query': {
|
||||
handler() {
|
||||
this.prepareUserSession()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCartStore, ['loadCart'])
|
||||
...mapActions(useCartStore, ['loadCart']),
|
||||
...mapActions(useUserStore, ['hydrateFromRoute', 'fetchBridgeToken']),
|
||||
prepareUserSession() {
|
||||
this.sessionLoading = true
|
||||
this.hydrateFromRoute(this.$route.query)
|
||||
this.fetchBridgeToken().catch(error => {
|
||||
console.warn('获取平台授权令牌失败', error)
|
||||
this.$toast(error.message || '供应商验证失败,请稍后再试')
|
||||
}).finally(() => {
|
||||
this.sessionLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,767 @@
|
||||
<template>
|
||||
<main class="after-sale-detail-page">
|
||||
<van-nav-bar title="售后详情" left-arrow @click-left="$router.replace('/joyful-selection/merchant')" />
|
||||
|
||||
<section v-if="loading" class="skeleton-card">
|
||||
<van-skeleton title :row="3" />
|
||||
<van-skeleton title :row="5" />
|
||||
<van-skeleton title :row="3" />
|
||||
</section>
|
||||
|
||||
<section v-show="!loading" class="status-card" :class="statusCardClass">
|
||||
<div class="status-icon">
|
||||
<van-icon :name="statusIcon" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ statusTitle }}</h2>
|
||||
<p>{{ statusDesc }}</p>
|
||||
<button v-if="afterSale.auditRemark" type="button" @click="showAuditRemark">查看审核意见</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-show="!loading" class="detail-card progress-card">
|
||||
<h3>售后进度</h3>
|
||||
<div class="timeline">
|
||||
<div v-for="step in progressSteps" :key="step.key" class="timeline-item" :class="step.state">
|
||||
<span class="timeline-dot">
|
||||
<van-icon v-if="step.state === 'done'" name="success" />
|
||||
</span>
|
||||
<div class="timeline-main">
|
||||
<b>{{ step.title }}</b>
|
||||
<em>{{ step.time || step.subTitle }}</em>
|
||||
</div>
|
||||
<p>{{ step.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-show="!loading" class="detail-card product-card">
|
||||
<h3>售后商品</h3>
|
||||
<article v-for="item in products" :key="item.id" class="product-row">
|
||||
<van-image class="product-thumb" :src="item.image" fit="cover" />
|
||||
<div class="product-info">
|
||||
<h4>{{ item.name || '-' }}</h4>
|
||||
<p>{{ skuText(item) }}</p>
|
||||
<b>¥{{ item.price || afterSale.amount || '0.00' }}</b>
|
||||
</div>
|
||||
<span>x{{ item.number || 1 }}</span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section v-if="!loading && showReturnInfo" class="detail-card address-card">
|
||||
<h3>{{ returnInfoTitle }}</h3>
|
||||
<p><span>联系人</span><b>{{ afterSale.pickupName || '-' }}</b></p>
|
||||
<p><span>联系电话</span><b>{{ maskedMobile(afterSale.pickupMobile) }}</b></p>
|
||||
<p><span>地址</span><b>{{ afterSale.pickupAddress || '-' }}</b></p>
|
||||
<p v-if="afterSale.userLogisticsNo" class="logistics-row">
|
||||
<span>寄回单号</span><b>{{ afterSale.userLogisticsCompany || '快递' }} {{ afterSale.userLogisticsNo }}</b>
|
||||
</p>
|
||||
<p v-if="afterSale.merchantLogisticsNo" class="logistics-row">
|
||||
<span>换货单号</span><b>{{ afterSale.merchantLogisticsCompany || '快递' }} {{ afterSale.merchantLogisticsNo }}</b>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section v-show="!loading" class="detail-card info-card">
|
||||
<h3>售后信息</h3>
|
||||
<p class="service-no-row"><span>售后单号</span><b class="scrollable-no">{{ afterSale.serviceOrderId || '-' }}</b><button type="button" @click="copyServiceNo">复制</button></p>
|
||||
<p><span>子订单号</span><b>{{ afterSale.orderNumberId || afterSale.subOrderId || '-' }}</b></p>
|
||||
<p><span>售后类型</span><b>{{ afterSale.serviceTypeText || '-' }}</b></p>
|
||||
<p><span>售后原因</span><b>{{ afterSale.reason || '-' }}</b></p>
|
||||
<p><span>问题描述</span><b>{{ afterSale.problemDescription || '-' }}</b></p>
|
||||
<p><span>申请时间</span><b>{{ afterSale.applyTime || '-' }}</b></p>
|
||||
<p v-if="!isUnsignedAfterSale"><span>取件方式</span><b>{{ pickupMethodText }}</b></p>
|
||||
<p v-if="!isUnsignedAfterSale"><span>取件地址</span><b>{{ pickupAddressText }}</b></p>
|
||||
<div v-if="images.length" class="voucher-block">
|
||||
<h4>上传凭证</h4>
|
||||
<div>
|
||||
<van-image v-for="image in images" :key="image.url" :src="image.url" fit="cover" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer v-show="!loading && merchantActions.length" class="bottom-actions">
|
||||
<button
|
||||
v-for="action in merchantActions"
|
||||
:key="action.key"
|
||||
type="button"
|
||||
:class="{ primary: action.primary }"
|
||||
@click="handleAction(action.key)"
|
||||
>
|
||||
{{ action.text }}
|
||||
</button>
|
||||
</footer>
|
||||
|
||||
<van-popup v-model="showExchangeShipPopup" round position="bottom">
|
||||
<section class="action-popup">
|
||||
<h3>填写换货发货信息</h3>
|
||||
<van-field v-model="exchangeShipForm.logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
||||
<van-field v-model="exchangeShipForm.logisticsNo" label="快递单号" placeholder="请输入快递单号" />
|
||||
<button type="button" @click="submitExchangeShip">确认发货</button>
|
||||
</section>
|
||||
</van-popup>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { Dialog, Toast } from 'vant'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const SERVICE_TYPE = {
|
||||
RETURN: 4,
|
||||
EXCHANGE: 5
|
||||
}
|
||||
|
||||
const SERVICE_STATE = {
|
||||
PENDING_AUDIT: 0,
|
||||
APPROVED: 1,
|
||||
REJECTED: 2,
|
||||
USER_SHIPPED: 3,
|
||||
RECEIVED: 4,
|
||||
FINISHED: 5,
|
||||
CANCELLED: 6
|
||||
}
|
||||
|
||||
const LOGISTICS_STATE = {
|
||||
NONE: 0,
|
||||
WAIT_USER_SHIP: 1,
|
||||
USER_SHIPPED: 2,
|
||||
MERCHANT_RECEIVED: 3,
|
||||
MERCHANT_SHIPPED: 4,
|
||||
USER_RECEIVED: 5
|
||||
}
|
||||
|
||||
const SKU_COLOR_MAP = {
|
||||
cream: '奶油白',
|
||||
blue: '雾霾蓝',
|
||||
orange: '暖橙色'
|
||||
}
|
||||
|
||||
const SKU_SIZE_MAP = {
|
||||
standard: '标准款',
|
||||
plus: '升级款'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionMerchantAfterSaleDetail',
|
||||
data: () => ({
|
||||
afterSale: {},
|
||||
loading: false,
|
||||
showExchangeShipPopup: false,
|
||||
exchangeShipForm: {
|
||||
logisticsCompany: '',
|
||||
logisticsNo: ''
|
||||
}
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId']),
|
||||
products() {
|
||||
return this.afterSale.products || []
|
||||
},
|
||||
images() {
|
||||
return Array.isArray(this.afterSale.images)
|
||||
? this.afterSale.images.map(item => ({
|
||||
url: item.url || item.previewUrl || item
|
||||
})).filter(item => item.url)
|
||||
: []
|
||||
},
|
||||
isExchange() {
|
||||
return Number(this.afterSale.serviceType) === SERVICE_TYPE.EXCHANGE
|
||||
},
|
||||
isUnsignedAfterSale() {
|
||||
if (this.afterSale.isSigned === false || this.afterSale.isSigned === 0 || this.afterSale.isSigned === 'false') {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.afterSale.isSigned === undefined &&
|
||||
Number(this.afterSale.logisticsState) === LOGISTICS_STATE.NONE &&
|
||||
!this.afterSale.pickupMethod &&
|
||||
!this.afterSale.pickupName &&
|
||||
!this.afterSale.pickupMobile &&
|
||||
!this.afterSale.pickupAddress
|
||||
},
|
||||
statusCardClass() {
|
||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.REJECTED) return 'rejected'
|
||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.APPROVED) return 'approved'
|
||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.FINISHED) return 'finished'
|
||||
return 'pending'
|
||||
},
|
||||
statusIcon() {
|
||||
if (this.statusCardClass === 'approved' || this.statusCardClass === 'finished') return 'success'
|
||||
if (this.statusCardClass === 'rejected') return 'cross'
|
||||
return 'clock-o'
|
||||
},
|
||||
statusTitle() {
|
||||
const serviceState = Number(this.afterSale.serviceState)
|
||||
const logisticsState = Number(this.afterSale.logisticsState)
|
||||
if (serviceState === SERVICE_STATE.REJECTED) return '审核未通过'
|
||||
if (serviceState === SERVICE_STATE.FINISHED) return '售后完成'
|
||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '商家已发货'
|
||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '待商家发货'
|
||||
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '用户已寄回'
|
||||
if (serviceState === SERVICE_STATE.APPROVED) return '审核通过'
|
||||
return '待审核'
|
||||
},
|
||||
statusDesc() {
|
||||
const serviceState = Number(this.afterSale.serviceState)
|
||||
const logisticsState = Number(this.afterSale.logisticsState)
|
||||
if (serviceState === SERVICE_STATE.REJECTED) return '商家已拒绝该售后申请'
|
||||
if (serviceState === SERVICE_STATE.FINISHED) return '售后流程已完成'
|
||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED) return '换货商品已发出,等待用户收货'
|
||||
if (logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED && this.isExchange) return '已收到寄回商品,请发出换货商品'
|
||||
if (logisticsState === LOGISTICS_STATE.USER_SHIPPED) return '用户已寄回商品,请确认收货'
|
||||
if (this.isExchange && this.isUnsignedAfterSale && serviceState === SERVICE_STATE.APPROVED) return '未签收换货申请已通过,请发出换货商品'
|
||||
if (serviceState === SERVICE_STATE.APPROVED) return '已通过售后申请,等待用户寄回商品'
|
||||
return '请审核用户提交的售后申请'
|
||||
},
|
||||
progressSteps() {
|
||||
const serviceState = Number(this.afterSale.serviceState)
|
||||
const logisticsState = Number(this.afterSale.logisticsState)
|
||||
const steps = [
|
||||
{
|
||||
key: 'apply',
|
||||
title: '提交申请',
|
||||
time: this.afterSale.applyTime,
|
||||
desc: '用户已提交售后申请',
|
||||
state: 'done'
|
||||
},
|
||||
{
|
||||
key: 'audit',
|
||||
title: '商家审核',
|
||||
time: this.afterSale.auditTime,
|
||||
desc: serviceState === SERVICE_STATE.REJECTED ? '商家审核未通过' : serviceState >= SERVICE_STATE.APPROVED ? '商家已审核通过' : '待商家审核',
|
||||
state: serviceState >= SERVICE_STATE.APPROVED || serviceState === SERVICE_STATE.REJECTED ? 'done' : 'current'
|
||||
},
|
||||
{
|
||||
key: 'userShip',
|
||||
title: '寄回商品',
|
||||
time: this.afterSale.userShipTime,
|
||||
desc: logisticsState >= LOGISTICS_STATE.USER_SHIPPED ? '用户已寄回商品' : '等待用户寄回商品',
|
||||
state: logisticsState >= LOGISTICS_STATE.USER_SHIPPED ? 'done' : logisticsState === LOGISTICS_STATE.WAIT_USER_SHIP ? 'current' : 'todo'
|
||||
},
|
||||
{
|
||||
key: 'merchantReceive',
|
||||
title: '商家收货',
|
||||
time: this.afterSale.merchantReceiveTime,
|
||||
desc: logisticsState >= LOGISTICS_STATE.MERCHANT_RECEIVED ? '商家已收到商品' : '商家尚未收到商品',
|
||||
state: logisticsState >= LOGISTICS_STATE.MERCHANT_RECEIVED ? 'done' : logisticsState === LOGISTICS_STATE.USER_SHIPPED ? 'current' : 'todo'
|
||||
}
|
||||
]
|
||||
|
||||
if (this.isUnsignedAfterSale && !this.isExchange) {
|
||||
steps.splice(2)
|
||||
steps.push({
|
||||
key: 'finish',
|
||||
title: '售后完成',
|
||||
time: this.afterSale.finishTime,
|
||||
desc: serviceState === SERVICE_STATE.FINISHED ? '售后已完成' : '审核通过后售后完成',
|
||||
state: serviceState === SERVICE_STATE.FINISHED ? 'done' : serviceState >= SERVICE_STATE.APPROVED ? 'current' : 'todo'
|
||||
})
|
||||
|
||||
return this.normalizeSteps(steps)
|
||||
}
|
||||
|
||||
if (this.isExchange) {
|
||||
if (this.isUnsignedAfterSale) {
|
||||
steps.splice(2)
|
||||
}
|
||||
|
||||
steps.push(
|
||||
{
|
||||
key: 'merchantShip',
|
||||
title: '商家发货',
|
||||
time: this.afterSale.merchantShipTime,
|
||||
desc: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? '商家已发出换货商品' : '等待商家发出换货商品',
|
||||
state: logisticsState >= LOGISTICS_STATE.MERCHANT_SHIPPED ? 'done' : (this.isUnsignedAfterSale ? serviceState >= SERVICE_STATE.APPROVED : logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED) ? 'current' : 'todo'
|
||||
},
|
||||
{
|
||||
key: 'userReceive',
|
||||
title: '用户收货',
|
||||
time: this.afterSale.finishTime,
|
||||
desc: logisticsState >= LOGISTICS_STATE.USER_RECEIVED ? '用户已收到换货商品' : '等待用户收货',
|
||||
state: logisticsState >= LOGISTICS_STATE.USER_RECEIVED ? 'done' : logisticsState === LOGISTICS_STATE.MERCHANT_SHIPPED ? 'current' : 'todo'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
steps.push({
|
||||
key: 'finish',
|
||||
title: '售后完成',
|
||||
time: this.afterSale.finishTime,
|
||||
desc: serviceState === SERVICE_STATE.FINISHED ? '售后流程已完成' : '售后流程未完成',
|
||||
state: serviceState === SERVICE_STATE.FINISHED ? 'done' : !this.isExchange && logisticsState === LOGISTICS_STATE.MERCHANT_RECEIVED ? 'current' : 'todo'
|
||||
})
|
||||
|
||||
return this.normalizeSteps(steps)
|
||||
},
|
||||
showReturnInfo() {
|
||||
return Number(this.afterSale.serviceState) >= SERVICE_STATE.APPROVED && !this.isUnsignedAfterSale
|
||||
},
|
||||
returnInfoTitle() {
|
||||
return Number(this.afterSale.logisticsState) >= LOGISTICS_STATE.WAIT_USER_SHIP ? '退货地址' : '取件地址'
|
||||
},
|
||||
pickupMethodText() {
|
||||
return this.afterSale.pickupMethod === 'mail' ? '自行寄回' : '上门取件'
|
||||
},
|
||||
pickupAddressText() {
|
||||
return this.afterSale.pickupAddress || '-'
|
||||
},
|
||||
merchantActions() {
|
||||
const serviceState = Number(this.afterSale.serviceState)
|
||||
if (serviceState === SERVICE_STATE.PENDING_AUDIT) {
|
||||
return [
|
||||
{ key: 'reject', text: '拒绝' },
|
||||
{ key: 'approve', text: '通过', primary: true }
|
||||
]
|
||||
}
|
||||
|
||||
if (Number(this.afterSale.serviceState) === SERVICE_STATE.USER_SHIPPED) {
|
||||
return [{ key: 'receive', text: '确认收货', primary: true }]
|
||||
}
|
||||
|
||||
if (this.canShipExchange) {
|
||||
return [{ key: 'shipExchange', text: '换货发货', primary: true }]
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
canShipExchange() {
|
||||
return this.isExchange &&
|
||||
(Number(this.afterSale.serviceState) === SERVICE_STATE.RECEIVED ||
|
||||
(this.afterSale.isSigned === false && Number(this.afterSale.serviceState) === SERVICE_STATE.APPROVED))
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadDetail()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
normalizeSteps(steps) {
|
||||
const serviceState = Number(this.afterSale.serviceState)
|
||||
return steps.map((step, index) => ({
|
||||
...step,
|
||||
state: serviceState === SERVICE_STATE.REJECTED && index > 1 ? 'todo' : step.state,
|
||||
subTitle: step.state === 'done' ? '' : '-'
|
||||
}))
|
||||
},
|
||||
skuText(item = {}) {
|
||||
const sku = String(item.sku || item.skuText || '').trim()
|
||||
const parts = sku.split('-').slice(-2).filter(Boolean)
|
||||
const color = SKU_COLOR_MAP[parts[0]] || parts[0]
|
||||
const size = SKU_SIZE_MAP[parts[1]] || parts[1]
|
||||
|
||||
return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
|
||||
},
|
||||
maskedMobile(mobile = '') {
|
||||
return String(mobile).replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2') || '-'
|
||||
},
|
||||
async loadDetail() {
|
||||
try {
|
||||
this.loading = true
|
||||
await this.fetchBridgeToken()
|
||||
const data = await postFunction('merchant-after-sale-detail', {
|
||||
supplierId: this.supplierId,
|
||||
serviceOrderId: String(this.$route.query.serviceOrderId || '')
|
||||
})
|
||||
|
||||
this.afterSale = data?.afterSale || {}
|
||||
} catch (error) {
|
||||
Toast(error.message || '查询售后详情失败,请稍后再试')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
copyServiceNo() {
|
||||
if (navigator.clipboard && this.afterSale.serviceOrderId) {
|
||||
navigator.clipboard.writeText(this.afterSale.serviceOrderId)
|
||||
}
|
||||
Toast('已复制')
|
||||
},
|
||||
showAuditRemark() {
|
||||
Dialog.alert({
|
||||
title: '审核意见',
|
||||
message: this.afterSale.auditRemark
|
||||
})
|
||||
},
|
||||
handleAction(key) {
|
||||
if (key === 'approve') {
|
||||
this.auditAfterSale(1)
|
||||
return
|
||||
}
|
||||
if (key === 'reject') {
|
||||
this.auditAfterSale(2)
|
||||
return
|
||||
}
|
||||
if (key === 'receive') {
|
||||
this.receiveAfterSale()
|
||||
return
|
||||
}
|
||||
if (key === 'shipExchange') {
|
||||
this.openExchangeShipPopup()
|
||||
}
|
||||
},
|
||||
async auditAfterSale(auditStatus) {
|
||||
await Dialog.confirm({
|
||||
title: auditStatus === 1 ? '确认通过售后申请?' : '确认拒绝售后申请?',
|
||||
message: auditStatus === 1 ? '通过后用户可寄回商品。' : '拒绝后该售后申请将结束。'
|
||||
})
|
||||
await postFunction('merchant-after-sale-audit', {
|
||||
supplierId: this.supplierId,
|
||||
serviceOrderId: this.afterSale.serviceOrderId,
|
||||
auditStatus,
|
||||
auditRemark: auditStatus === 1 ? '审核通过' : '商家拒绝'
|
||||
})
|
||||
Toast('处理成功')
|
||||
await this.loadDetail()
|
||||
},
|
||||
async receiveAfterSale() {
|
||||
await Dialog.confirm({
|
||||
title: '确认已收到寄回商品?',
|
||||
message: '确认后退货售后会进入完成,换货售后会进入待商家寄货。'
|
||||
})
|
||||
await postFunction('merchant-after-sale-receive', {
|
||||
supplierId: this.supplierId,
|
||||
serviceOrderId: this.afterSale.serviceOrderId
|
||||
})
|
||||
Toast('已确认收货')
|
||||
await this.loadDetail()
|
||||
},
|
||||
openExchangeShipPopup() {
|
||||
this.exchangeShipForm = {
|
||||
logisticsCompany: '',
|
||||
logisticsNo: ''
|
||||
}
|
||||
this.showExchangeShipPopup = true
|
||||
},
|
||||
async submitExchangeShip() {
|
||||
if (!this.exchangeShipForm.logisticsCompany || !this.exchangeShipForm.logisticsNo) {
|
||||
Toast('请填写完整发货信息')
|
||||
return
|
||||
}
|
||||
|
||||
await postFunction('merchant-after-sale-ship', {
|
||||
supplierId: this.supplierId,
|
||||
serviceOrderId: this.afterSale.serviceOrderId,
|
||||
logisticsCompany: this.exchangeShipForm.logisticsCompany,
|
||||
logisticsNo: this.exchangeShipForm.logisticsNo
|
||||
})
|
||||
Toast('换货发货成功')
|
||||
this.showExchangeShipPopup = false
|
||||
await this.loadDetail()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.after-sale-detail-page {
|
||||
min-height: 100vh;
|
||||
padding: 56px 10px 78px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.after-sale-detail-page ::v-deep .van-nav-bar {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.skeleton-card,
|
||||
.detail-card {
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
padding: 18px 14px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 18px 14px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: none;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
background: #ff9f1a;
|
||||
}
|
||||
|
||||
.status-card.approved .status-icon,
|
||||
.status-card.finished .status-icon {
|
||||
background: #20b879;
|
||||
}
|
||||
|
||||
.status-card.rejected .status-icon {
|
||||
background: #ee3f4d;
|
||||
}
|
||||
|
||||
.status-card h2 {
|
||||
margin: 0 0 6px;
|
||||
color: #222;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.status-card p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.status-card button {
|
||||
margin-top: 8px;
|
||||
border: 0;
|
||||
color: #ff5b20;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.info-card button {
|
||||
flex: none;
|
||||
align-self: center;
|
||||
height: 24px;
|
||||
margin-left: 8px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: #ff5b20;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
padding: 14px 12px;
|
||||
}
|
||||
|
||||
.detail-card h3 {
|
||||
margin: 0 0 12px;
|
||||
color: #222;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
bottom: -2px;
|
||||
left: 6px;
|
||||
width: 1px;
|
||||
background: #e5e5e5;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-item:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
background: #d8d8d8;
|
||||
}
|
||||
|
||||
.timeline-item.done .timeline-dot {
|
||||
background: #20b879;
|
||||
}
|
||||
|
||||
.timeline-item.current .timeline-dot {
|
||||
background: #ff5b20;
|
||||
}
|
||||
|
||||
.timeline-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.timeline-main b {
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.timeline-main em,
|
||||
.timeline-item p {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.timeline-item p {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.product-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.product-thumb {
|
||||
flex: none;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-info h4 {
|
||||
margin: 0 0 6px;
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.product-info p {
|
||||
margin: 0 0 8px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.product-info b {
|
||||
color: #ff5b20;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.product-row > span {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.address-card p,
|
||||
.info-card p {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 10px 0 0;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.address-card b,
|
||||
.info-card b {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.info-card .service-no-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-card .scrollable-no {
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.info-card .scrollable-no::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.voucher-block h4 {
|
||||
margin: 14px 0 8px;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.voucher-block div {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.voucher-block .van-image {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
box-shadow: 0 -4px 14px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.bottom-actions button,
|
||||
.action-popup button {
|
||||
min-width: 96px;
|
||||
height: 36px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 18px;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.bottom-actions button.primary,
|
||||
.action-popup button {
|
||||
border-color: #ff5b20;
|
||||
color: #fff;
|
||||
background: #ff5b20;
|
||||
}
|
||||
|
||||
.action-popup {
|
||||
padding: 18px 14px 22px;
|
||||
}
|
||||
|
||||
.action-popup h3 {
|
||||
margin: 0 0 12px;
|
||||
color: #222;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-popup button {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,677 @@
|
||||
<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" :badge="tabBadge('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>{{ skuText(product) }}</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" :badge="tabBadge('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 audit-card" @click="goAfterSaleDetail(item)">
|
||||
<header>
|
||||
<b>{{ item.serviceOrderId }}</b>
|
||||
<span>{{ item.serviceTypeText }}</span>
|
||||
</header>
|
||||
<span class="signed-stamp" :class="{ unsigned: item.isSigned === false }">{{ item.isSigned === false ? '未签收' : '已签收' }}</span>
|
||||
<div v-for="product in item.products" :key="product.id" class="product-row">
|
||||
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
||||
<div class="product-info">
|
||||
<h4>{{ product.name || '-' }}</h4>
|
||||
<p>{{ skuText(product) }}</p>
|
||||
<b>¥{{ product.price }}</b>
|
||||
</div>
|
||||
<em>x{{ product.number || 1 }}</em>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>原因</span><b>{{ item.reason || '-' }}</b>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>描述</span><b>{{ item.problemDescription || '-' }}</b>
|
||||
</div>
|
||||
<footer>
|
||||
<button type="button" @click.stop="goAfterSaleDetail(item)">查看详情</button>
|
||||
<button type="button" @click.stop="auditAfterSale(item, 2)">拒绝</button>
|
||||
<button type="button" class="primary" @click.stop="auditAfterSale(item, 1)">通过</button>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
</section>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="售后处理" name="process" :badge="tabBadge('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" @click="goAfterSaleDetail(item)">
|
||||
<header>
|
||||
<b>{{ item.serviceOrderId }}</b>
|
||||
<span>{{ item.serviceStateText }}</span>
|
||||
</header>
|
||||
<span class="signed-stamp" :class="{ unsigned: item.isSigned === false }">{{ item.isSigned === false ? '未签收' : '已签收' }}</span>
|
||||
<div v-for="product in item.products" :key="product.id" class="product-row">
|
||||
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
||||
<div class="product-info">
|
||||
<h4>{{ product.name || '-' }}</h4>
|
||||
<p>{{ skuText(product) }}</p>
|
||||
<b>¥{{ product.price }}</b>
|
||||
</div>
|
||||
<em>x{{ product.number || 1 }}</em>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>寄回单号</span><b>{{ item.userLogisticsCompany || '' }} {{ item.userLogisticsNo || '-' }}</b>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>取件地址</span><b>{{ item.pickupAddress || '-' }}</b>
|
||||
</div>
|
||||
<footer>
|
||||
<button type="button" @click.stop="goAfterSaleDetail(item)">查看详情</button>
|
||||
<button v-if="canReceiveAfterSale(item)" type="button" class="primary" @click.stop="receiveAfterSale(item)">确认收货</button>
|
||||
<button v-if="canShipExchange(item)" type="button" class="primary" @click.stop="openExchangeShipPopup(item)">换货发货</button>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
</section>
|
||||
</van-tab>
|
||||
|
||||
<van-tab title="历史售后" name="history">
|
||||
<section class="list-panel">
|
||||
<section v-if="loading" class="skeleton-card">
|
||||
<van-skeleton title :row="3" />
|
||||
<van-skeleton title :row="3" />
|
||||
</section>
|
||||
<van-empty v-else-if="historyItems.length === 0" description="暂无历史售后" />
|
||||
<template v-else>
|
||||
<article v-for="item in historyItems" :key="item.serviceOrderId" class="work-card" @click="goAfterSaleDetail(item)">
|
||||
<header>
|
||||
<b>{{ item.serviceOrderId }}</b>
|
||||
<span>{{ item.serviceStateText }}</span>
|
||||
</header>
|
||||
<span class="signed-stamp" :class="{ unsigned: item.isSigned === false }">{{ item.isSigned === false ? '未签收' : '已签收' }}</span>
|
||||
<div v-for="product in item.products" :key="product.id" class="product-row">
|
||||
<van-image class="product-thumb" :src="product.image" fit="cover" />
|
||||
<div class="product-info">
|
||||
<h4>{{ product.name || '-' }}</h4>
|
||||
<p>{{ skuText(product) }}</p>
|
||||
<b>¥{{ product.price }}</b>
|
||||
</div>
|
||||
<em>x{{ product.number || 1 }}</em>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>类型</span><b>{{ item.serviceTypeText || '-' }}</b>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>原因</span><b>{{ item.reason || '-' }}</b>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span>申请时间</span><b>{{ item.applyTime || '-' }}</b>
|
||||
</div>
|
||||
<footer>
|
||||
<button type="button" class="primary" @click.stop="goAfterSaleDetail(item)">查看详情</button>
|
||||
</footer>
|
||||
</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'
|
||||
import { useUiStore } from '@/store/ui'
|
||||
|
||||
const ORDER_STATE = {
|
||||
FINISH_PAY: 1
|
||||
}
|
||||
|
||||
const SERVICE_TYPE = {
|
||||
EXCHANGE: 5
|
||||
}
|
||||
|
||||
const SERVICE_STATE = {
|
||||
APPROVED: 1,
|
||||
USER_SHIPPED: 3,
|
||||
RECEIVED: 4
|
||||
}
|
||||
|
||||
const SKU_COLOR_MAP = {
|
||||
cream: '奶油白',
|
||||
blue: '雾霾蓝',
|
||||
orange: '暖橙色'
|
||||
}
|
||||
|
||||
const SKU_SIZE_MAP = {
|
||||
standard: '标准款',
|
||||
plus: '升级款'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionMerchant',
|
||||
data: () => ({
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
orderItems: [],
|
||||
auditItems: [],
|
||||
processItems: [],
|
||||
historyItems: [],
|
||||
currentOrder: null,
|
||||
currentAfterSale: null,
|
||||
showShipPopup: false,
|
||||
showExchangeShipPopup: false,
|
||||
shipForm: {
|
||||
crrgBsnNm: '',
|
||||
logisticsOrderId: ''
|
||||
},
|
||||
exchangeShipForm: {
|
||||
logisticsCompany: '',
|
||||
logisticsNo: ''
|
||||
}
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId']),
|
||||
...mapState(useUiStore, ['merchantTab']),
|
||||
activeTab: {
|
||||
get() {
|
||||
return this.merchantTab
|
||||
},
|
||||
set(value) {
|
||||
this.setMerchantTab(value)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadUi()
|
||||
this.prepareSupplier().then(() => this.loadAllTabs())
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchSupplierInfo', 'fetchBridgeToken']),
|
||||
...mapActions(useUiStore, ['loadUi', 'setMerchantTab']),
|
||||
skuText(item = {}) {
|
||||
const sku = String(item.sku || item.skuText || '').trim()
|
||||
const parts = sku.split('-').slice(-2).filter(Boolean)
|
||||
const color = SKU_COLOR_MAP[parts[0]] || parts[0]
|
||||
const size = SKU_SIZE_MAP[parts[1]] || parts[1]
|
||||
|
||||
return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
|
||||
},
|
||||
tabBadge(name) {
|
||||
const countMap = {
|
||||
ship: this.orderItems.length,
|
||||
audit: this.auditItems.length,
|
||||
process: this.processItems.length
|
||||
}
|
||||
const count = countMap[name] || 0
|
||||
|
||||
return count > 0 ? count : ''
|
||||
},
|
||||
async prepareSupplier() {
|
||||
await this.fetchBridgeToken()
|
||||
if (!this.supplierId) {
|
||||
await this.fetchSupplierInfo()
|
||||
}
|
||||
},
|
||||
async loadAllTabs() {
|
||||
this.loading = true
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadOrders(),
|
||||
this.loadAfterSales('audit'),
|
||||
this.loadAfterSales('process'),
|
||||
this.loadAfterSales('history')
|
||||
])
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async loadCurrentTab(showLoading = true) {
|
||||
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 if (this.activeTab === 'process') {
|
||||
await this.loadAfterSales('process').finally(() => {
|
||||
if (shouldShowLoading) {
|
||||
this.loading = false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await this.loadAfterSales('history').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 if (scene === 'history') {
|
||||
this.historyItems = data?.afterSaleItems || []
|
||||
} else {
|
||||
this.processItems = data?.afterSaleItems || []
|
||||
}
|
||||
},
|
||||
goAfterSaleDetail(item) {
|
||||
this.$router.push({
|
||||
path: '/joyful-selection/merchant/after-sale-detail',
|
||||
query: {
|
||||
serviceOrderId: item.serviceOrderId
|
||||
}
|
||||
})
|
||||
},
|
||||
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 ||
|
||||
(item.isSigned === false && Number(item.serviceState) === SERVICE_STATE.APPROVED))
|
||||
},
|
||||
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 {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
padding: 14px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.audit-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.signed-stamp {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 110px;
|
||||
right: 45px;
|
||||
display: flex;
|
||||
width: 95px;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid rgba(255, 91, 32, .8);
|
||||
border-radius: 50%;
|
||||
color: #ff5b20;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
opacity: .9;
|
||||
background: rgba(255, 241, 232, .55);
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.signed-stamp::before {
|
||||
position: absolute;
|
||||
inset: 4px;
|
||||
border: 1px dashed currentColor;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.signed-stamp.unsigned {
|
||||
border-color: rgba(153, 153, 153, .9);
|
||||
color: #666;
|
||||
background: rgba(246, 247, 249, .7);
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.work-card header b::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.work-card header span {
|
||||
flex: none;
|
||||
margin-left: 8px;
|
||||
color: #ff5b20;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.product-row {
|
||||
display: flex;
|
||||
position: relative;
|
||||
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: 88px;
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
background: #f6f7f9;
|
||||
}
|
||||
|
||||
.work-card footer .primary {
|
||||
color: #ff5b20;
|
||||
background: #fff1e8;
|
||||
}
|
||||
|
||||
.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,15 +1,25 @@
|
||||
<template>
|
||||
<main class="checkout-page">
|
||||
<div>
|
||||
<joyful-selection-checkout-skeleton v-if="loading" />
|
||||
<main v-else class="checkout-page">
|
||||
<van-nav-bar title="确认订单" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<section class="address-card" @click="$router.push({ path: '/joyful-selection/address', query: { from: 'checkout' } })">
|
||||
<section class="address-card" :class="{ 'address-card--empty': !chosenAddress.id }" @click="goAddress">
|
||||
<van-icon class="address-icon" name="location" />
|
||||
<div class="address-info">
|
||||
<div v-if="chosenAddress.id" class="address-info">
|
||||
<h3>收货人:{{ chosenAddress.name }} <span>{{ maskTel(chosenAddress.tel) }}</span></h3>
|
||||
<p>{{ chosenAddress.province }}{{ chosenAddress.city }}{{ chosenAddress.county }}</p>
|
||||
<p>{{ chosenAddress.addressDetail }}</p>
|
||||
</div>
|
||||
<van-icon class="arrow-icon" name="arrow" />
|
||||
<div v-else class="address-info address-empty">
|
||||
<h3>请先新增收货地址</h3>
|
||||
<p>用于商品配送,请确保信息准确</p>
|
||||
</div>
|
||||
<div v-if="!chosenAddress.id" class="address-action">
|
||||
<span>去新增</span>
|
||||
<van-icon name="arrow" />
|
||||
</div>
|
||||
<van-icon v-else class="arrow-icon" name="arrow" />
|
||||
</section>
|
||||
|
||||
<section class="delivery-card">
|
||||
@@ -28,7 +38,8 @@
|
||||
<van-image class="order-thumb" :src="item.image" fit="cover" />
|
||||
<div class="order-info">
|
||||
<h3>{{ item.name }}</h3>
|
||||
<p>¥{{ item.price }}</p>
|
||||
<p v-if="productSpecText(item)" class="order-desc">{{ productSpecText(item) }}</p>
|
||||
<b>¥{{ item.price }}</b>
|
||||
</div>
|
||||
<span class="order-count">x{{ item.count || 1 }}</span>
|
||||
</article>
|
||||
@@ -46,31 +57,40 @@
|
||||
<section class="amount-card">
|
||||
<p><span>商品金额</span><b>¥{{ formatYuan(goodsAmount) }}</b></p>
|
||||
<p><span>运费</span><b>¥0.00</b></p>
|
||||
<p><span>优惠券</span><b class="muted">选择优惠券</b></p>
|
||||
<p class="coupon-row" @click="showCouponTip"><span>优惠券</span><b class="muted">选择优惠券<van-icon name="arrow" /></b></p>
|
||||
</section>
|
||||
|
||||
<van-submit-bar :price="goodsAmount" button-text="提交订单" @submit="onSubmit" />
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import { readAddresses, readChosenAddressId } from '../../address/address-data'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { createOrderNo, savePaymentInfo } from '../payment/payment-data'
|
||||
import JoyfulSelectionCheckoutSkeleton from '@/skeleton-view/joyful-selection/order/checkout/index.vue'
|
||||
|
||||
const CHECKOUT_ITEMS_KEY = 'joyful-selection-checkout-items'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCheckout',
|
||||
components: {
|
||||
JoyfulSelectionCheckoutSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
remark: '',
|
||||
goods: [],
|
||||
showAllGoods: false,
|
||||
chosenAddress: {}
|
||||
chosenAddress: {},
|
||||
submitting: false
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useCartStore, ['items']),
|
||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||
visibleGoods() {
|
||||
return this.showAllGoods ? this.goods : this.goods.slice(0, 3)
|
||||
},
|
||||
@@ -84,13 +104,20 @@ export default {
|
||||
created() {
|
||||
this.loadCart()
|
||||
this.goods = this.getCheckoutGoods()
|
||||
this.loadAddress()
|
||||
this.loadAddress().catch(error => {
|
||||
this.$toast(error.message || '查询地址失败,请稍后再试')
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
activated() {
|
||||
this.loadAddress()
|
||||
this.loadAddress().catch(error => {
|
||||
this.$toast(error.message || '查询地址失败,请稍后再试')
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCartStore, ['loadCart']),
|
||||
...mapActions(useCartStore, ['loadCart', 'removeItems']),
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
getCheckoutGoods() {
|
||||
const storedItems = this.readCheckoutItems()
|
||||
|
||||
@@ -114,23 +141,96 @@ export default {
|
||||
formatYuan(price) {
|
||||
return (Number(price || 0) / 100).toFixed(2)
|
||||
},
|
||||
loadAddress() {
|
||||
const addresses = readAddresses()
|
||||
const chosenId = readChosenAddressId()
|
||||
this.chosenAddress = addresses.find(item => item.id === chosenId) || addresses[0] || {}
|
||||
productSpecText(item = {}) {
|
||||
return String(item.skuText || item.desc || '').replace(/\s*\/\s*/g, ' | ')
|
||||
},
|
||||
async loadAddress() {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId) {
|
||||
this.chosenAddress = {}
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('address-default', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId
|
||||
})
|
||||
this.chosenAddress = data?.address || {}
|
||||
},
|
||||
goAddress() {
|
||||
if (this.chosenAddress.id) {
|
||||
this.$router.push({ path: '/joyful-selection/address', query: { from: 'checkout' } })
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push({ path: '/joyful-selection/address/edit', query: { from: 'checkout' } })
|
||||
},
|
||||
maskTel(tel = '') {
|
||||
return tel.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')
|
||||
},
|
||||
onSubmit() {
|
||||
savePaymentInfo({
|
||||
orderNo: createOrderNo(),
|
||||
amount: this.goodsAmount,
|
||||
receiverName: this.chosenAddress.name || '',
|
||||
receiverTel: this.maskTel(this.chosenAddress.tel || ''),
|
||||
goods: this.goods
|
||||
})
|
||||
this.$router.push('/joyful-selection/payment')
|
||||
showCouponTip() {
|
||||
this.$toast('急速开发中,敬请期待')
|
||||
},
|
||||
async onSubmit() {
|
||||
if (this.submitting) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.goods.length) {
|
||||
this.$toast('暂无可提交的商品')
|
||||
return
|
||||
}
|
||||
|
||||
this.submitting = true
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId) {
|
||||
throw new Error('供应商授权信息不完整,请重新进入商城')
|
||||
}
|
||||
|
||||
const orderNo = createOrderNo()
|
||||
const data = await postFunction('order-create', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
orderNo,
|
||||
amount: this.goodsAmount,
|
||||
addressId: this.chosenAddress.id || '',
|
||||
remark: this.remark,
|
||||
goods: this.goods
|
||||
})
|
||||
|
||||
savePaymentInfo({
|
||||
orderNo: data?.orderId || orderNo,
|
||||
orderNumberId: data?.orderNumberId || '',
|
||||
amount: this.goodsAmount,
|
||||
receiverName: this.chosenAddress.name || '',
|
||||
receiverTel: this.maskTel(this.chosenAddress.tel || ''),
|
||||
goods: this.goods
|
||||
})
|
||||
|
||||
this.clearSubmittedCartItems()
|
||||
this.$router.push('/joyful-selection/payment')
|
||||
} catch (error) {
|
||||
console.warn('提交待支付订单失败', error)
|
||||
this.$toast(error.message || '提交待支付订单失败')
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
clearSubmittedCartItems() {
|
||||
if (this.$route.query.source !== 'cart') {
|
||||
return
|
||||
}
|
||||
|
||||
const cartIds = this.goods.map(item => item.cartId).filter(Boolean)
|
||||
|
||||
if (cartIds.length) {
|
||||
this.removeItems(cartIds)
|
||||
}
|
||||
|
||||
sessionStorage.removeItem(CHECKOUT_ITEMS_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,6 +269,10 @@ export default {
|
||||
padding: 14px 12px;
|
||||
}
|
||||
|
||||
.address-card--empty {
|
||||
background: #fff6ea;
|
||||
}
|
||||
|
||||
.address-icon {
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
@@ -203,6 +307,34 @@ export default {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.address-empty h3 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.address-empty p {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.address-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
height: 30px;
|
||||
margin-left: 8px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #ff6a2a;
|
||||
border-radius: 16px;
|
||||
color: #ff5b20;
|
||||
font-size: 12px;
|
||||
background: #fff6ea;
|
||||
}
|
||||
|
||||
.address-action .van-icon {
|
||||
margin-left: 4px;
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
flex: none;
|
||||
color: #999;
|
||||
@@ -299,13 +431,26 @@ export default {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.order-info p {
|
||||
.order-info b {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
color: #ff5b20;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-info .order-desc {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4px;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 17px;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.order-count {
|
||||
flex: none;
|
||||
color: #222;
|
||||
@@ -361,10 +506,22 @@ export default {
|
||||
}
|
||||
|
||||
.amount-card .muted {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #777;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.coupon-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.coupon-row .van-icon {
|
||||
margin-left: 4px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.checkout-page ::v-deep .van-submit-bar {
|
||||
bottom: 0;
|
||||
height: 58px;
|
||||
|
||||
@@ -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,26 +1,42 @@
|
||||
<template>
|
||||
<main class="success-page">
|
||||
<van-icon name="checked" color="#20bd58" size="68" />
|
||||
<h2>支付成功</h2>
|
||||
<b>¥ {{ amountText }}</b>
|
||||
<section>
|
||||
<p>订单编号 <span>{{ paymentInfo.orderNo || '-' }}</span></p>
|
||||
<p>支付方式 <span>{{ payTypeText }}</span></p>
|
||||
<p>支付时间 <span>{{ paidAt }}</span></p>
|
||||
<div>
|
||||
<joyful-selection-payment-success-skeleton v-if="loading" />
|
||||
<main v-else class="success-page">
|
||||
<van-nav-bar title="支付成功" left-arrow @click-left="goProductList" />
|
||||
|
||||
<section class="success-card">
|
||||
<div class="success-icon">
|
||||
<van-icon name="success" />
|
||||
</div>
|
||||
<h2>支付成功</h2>
|
||||
<b>¥ {{ amountText }}</b>
|
||||
|
||||
<div class="order-info">
|
||||
<p><span>订单编号</span><em>{{ paymentInfo.orderNo || '-' }}</em></p>
|
||||
<p><span>支付方式</span><em>{{ payTypeText }}</em></p>
|
||||
<p><span>支付时间</span><em>{{ paidAt }}</em></p>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<van-button round plain @click="$router.push('/joyful-selection/orders')">查看订单</van-button>
|
||||
<van-button round type="danger" @click="$router.push('/joyful-selection/home')">继续购物</van-button>
|
||||
</footer>
|
||||
</section>
|
||||
<div>
|
||||
<van-button round @click="$router.push('/joyful-selection/orders')">查看订单</van-button>
|
||||
<van-button round type="danger" @click="$router.push('/joyful-selection/home')">继续购物</van-button>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { readPaymentInfo, formatYuan } from '../payment/payment-data'
|
||||
import JoyfulSelectionPaymentSuccessSkeleton from '@/skeleton-view/joyful-selection/order/payment-success/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPaymentSuccess',
|
||||
components: {
|
||||
JoyfulSelectionPaymentSuccessSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
paymentInfo: {},
|
||||
paidAt: ''
|
||||
}),
|
||||
@@ -41,8 +57,14 @@ export default {
|
||||
created() {
|
||||
this.paymentInfo = readPaymentInfo()
|
||||
this.paidAt = this.formatTime(new Date())
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
goProductList() {
|
||||
this.$router.replace('/joyful-selection/products')
|
||||
},
|
||||
formatTime(date) {
|
||||
const pad = value => String(value).padStart(2, '0')
|
||||
|
||||
@@ -52,6 +74,105 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.success-page{min-height:100vh;padding:80px 30px;text-align:center;background:#fff}.success-page h2{margin:15px 0 10px;font-size:17px}.success-page>b{font-size:22px}.success-page section{margin-top:42px;text-align:left;color:#888;font-size:12px}.success-page section p{display:flex;justify-content:space-between}.success-page section span{color:#333}.success-page>div{display:flex;gap:10px;margin-top:35px}.success-page .van-button{flex:1}.success-page .van-button--danger{background:#ff5b20;border-color:#ff5b20}
|
||||
<style scoped lang="less">
|
||||
.success-page {
|
||||
min-height: 100vh;
|
||||
padding: 46px 8px 24px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.success-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);
|
||||
}
|
||||
|
||||
.success-card {
|
||||
min-height: calc(100vh - 70px);
|
||||
margin-top: 8px;
|
||||
padding: 34px 20px 24px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
display: flex;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.success-icon .van-icon {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.success-card h2 {
|
||||
margin: 14px 0 9px;
|
||||
color: #222;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.success-card > b {
|
||||
display: block;
|
||||
color: #222;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
margin-top: 34px;
|
||||
}
|
||||
|
||||
.order-info p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
.order-info em {
|
||||
max-width: 68%;
|
||||
overflow: hidden;
|
||||
color: #333;
|
||||
font-style: normal;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.success-card footer {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.success-card footer .van-button {
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.success-card ::v-deep .van-button--plain {
|
||||
color: #333;
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.success-card ::v-deep .van-button--danger {
|
||||
background: #ff5b20;
|
||||
border-color: #ff5b20;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="payment-page">
|
||||
<div>
|
||||
<joyful-selection-payment-skeleton v-if="loading" />
|
||||
<main v-else class="payment-page">
|
||||
<van-nav-bar title="确认支付" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<section class="amount-card">
|
||||
@@ -37,21 +39,55 @@
|
||||
</van-radio-group>
|
||||
</section>
|
||||
|
||||
<van-button class="pay-button" round block @click="onPay">立即支付 ¥{{ amountText }}</van-button>
|
||||
</main>
|
||||
<van-button class="pay-button" round block :loading="submitting" @click="onPay">
|
||||
立即支付 ¥{{ amountText }}
|
||||
</van-button>
|
||||
|
||||
<van-popup v-model="showConfirm" class="pay-popup" round position="bottom">
|
||||
<article class="pay-panel">
|
||||
<header>
|
||||
<van-icon name="cross" @click="showConfirm = false" />
|
||||
<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 :disabled="submitting" @click="showConfirm = false">取消</van-button>
|
||||
<van-button round type="danger" :loading="submitting" @click="confirmPay">确认支付</van-button>
|
||||
</footer>
|
||||
</article>
|
||||
</van-popup>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { readPaymentInfo, formatYuan } from './payment-data'
|
||||
import JoyfulSelectionPaymentSkeleton from '@/skeleton-view/joyful-selection/order/payment/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPayment',
|
||||
components: {
|
||||
JoyfulSelectionPaymentSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
payType: 'points',
|
||||
paymentInfo: {},
|
||||
pointsBalance: '32.00'
|
||||
showConfirm: false,
|
||||
submitting: false,
|
||||
paymentInfo: {}
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['pointsBalance', 'supplierId']),
|
||||
amountText() {
|
||||
return formatYuan(this.paymentInfo.amount)
|
||||
},
|
||||
@@ -59,22 +95,72 @@ export default {
|
||||
const name = this.paymentInfo.receiverName || '-'
|
||||
const tel = this.paymentInfo.receiverTel || ''
|
||||
|
||||
return tel ? `${name} ${tel}` : name
|
||||
return tel ? `${name} ${tel}` : name
|
||||
},
|
||||
payTypeText() {
|
||||
const method = this.payMethods.find(item => item.name === this.payType)
|
||||
|
||||
return method ? method.label.replace(/(.*$/, '') : '积分支付'
|
||||
},
|
||||
payMethods() {
|
||||
return [
|
||||
{ name: 'points', label: `积分支付(余额:¥${this.pointsBalance})`, icon: 'gold-coin-o' },
|
||||
{ name: 'points', label: `积分支付(余额:¥${this.pointsBalanceText})`, icon: 'gold-coin-o' },
|
||||
{ name: 'wechat', label: '微信支付', icon: 'wechat-pay' },
|
||||
{ name: 'alipay', label: '支付宝支付', icon: 'alipay' }
|
||||
]
|
||||
},
|
||||
pointsBalanceText() {
|
||||
return Number(this.pointsBalance || 0).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.paymentInfo = readPaymentInfo()
|
||||
this.loading = true
|
||||
this.fetchUserPoints().catch(error => {
|
||||
console.warn('查询用户积分失败', error)
|
||||
this.$toast(error.message || '查询用户积分失败')
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchUserPoints', 'fetchBridgeToken']),
|
||||
onPay() {
|
||||
this.$router.push({ path: '/joyful-selection/payment-confirm', query: { payType: this.payType } })
|
||||
this.showConfirm = true
|
||||
},
|
||||
async confirmPay() {
|
||||
if (this.submitting) {
|
||||
return
|
||||
}
|
||||
|
||||
this.submitting = true
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.paymentInfo.orderNo) {
|
||||
throw new Error('订单支付信息不完整,请重新提交订单')
|
||||
}
|
||||
|
||||
await postFunction('order-pay', {
|
||||
supplierId: this.supplierId,
|
||||
orderId: this.paymentInfo.orderNo,
|
||||
amount: this.paymentInfo.amount,
|
||||
totalPrice: Number(this.amountText),
|
||||
wPayOrAPay: 0,
|
||||
pointsPrice: Number(this.amountText)
|
||||
})
|
||||
|
||||
this.showConfirm = false
|
||||
this.$router.push({ path: '/joyful-selection/payment-success', query: { payType: this.payType } })
|
||||
} catch (error) {
|
||||
console.warn('确认购买失败', error)
|
||||
this.$toast(error.message || '确认购买失败')
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,4 +303,94 @@ export default {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.payment-page ::v-deep .van-button--danger {
|
||||
background: #ff5b20;
|
||||
border-color: #ff5b20;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ export const createOrderNo = () => {
|
||||
const pad = value => String(value).padStart(2, '0')
|
||||
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
||||
|
||||
return `YX${timestamp}`
|
||||
return `ORD${timestamp}`
|
||||
}
|
||||
|
||||
export const savePaymentInfo = info => {
|
||||
|
||||
@@ -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]
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="detail-page">
|
||||
<div>
|
||||
<joyful-selection-product-detail-skeleton v-if="loading" />
|
||||
<main v-else class="detail-page">
|
||||
<header class="detail-top">
|
||||
<van-icon name="arrow-left" @click="$router.back()" />
|
||||
<h1>商品详情</h1>
|
||||
@@ -11,7 +13,7 @@
|
||||
|
||||
<section class="image-section">
|
||||
<van-image class="product-image" :src="product.image" fit="cover" />
|
||||
<span class="image-index">1/5</span>
|
||||
<span class="image-index">1/{{ imageCount }}</span>
|
||||
</section>
|
||||
|
||||
<section class="product-info">
|
||||
@@ -77,30 +79,46 @@
|
||||
@checkout="goCheckout"
|
||||
/>
|
||||
</van-popup>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { getFunction } from '@/api/supplied-service'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import { getJoyfulProduct } from '../product-data'
|
||||
import JoyfulSkuPopup from '../products/components/JoyfulSkuPopup.vue'
|
||||
import JoyfulCartPanel from '../cart/components/JoyfulCartPanel.vue'
|
||||
import JoyfulSelectionProductDetailSkeleton from '@/skeleton-view/joyful-selection/product-detail/index.vue'
|
||||
|
||||
const emptyProduct = {
|
||||
id: '',
|
||||
name: '',
|
||||
detail: '',
|
||||
price: '0.00',
|
||||
originPrice: '0.00',
|
||||
image: '',
|
||||
picInfo: [],
|
||||
tag: ''
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProductDetail',
|
||||
components: {
|
||||
JoyfulSkuPopup,
|
||||
JoyfulCartPanel
|
||||
JoyfulCartPanel,
|
||||
JoyfulSelectionProductDetailSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
product: emptyProduct,
|
||||
showCartPopup: false,
|
||||
count: 1,
|
||||
features: [
|
||||
{ icon: 'music-o', title: '舒适半入耳设计', desc: '长时间佩戴更舒适' },
|
||||
{ icon: 'bar-chart-o', title: '高清音质', desc: '清晰稳定的聆听体验' },
|
||||
{ icon: 'battery-o', title: '长续航', desc: '单次可用约 5 小时' },
|
||||
{ icon: 'cluster-o', title: '蓝牙 5.0', desc: '稳定连接,低延迟' }
|
||||
{ icon: 'certificate', title: '品质优选', desc: '精选商品,安心兑换' },
|
||||
{ icon: 'logistics', title: '快速配送', desc: '下单后尽快安排发货' },
|
||||
{ icon: 'shield-o', title: '售后保障', desc: '商品问题及时处理' },
|
||||
{ icon: 'gold-coin-o', title: '积分抵扣', desc: '支持积分支付兑换' }
|
||||
]
|
||||
}),
|
||||
computed: {
|
||||
@@ -108,13 +126,28 @@ export default {
|
||||
cartBadge() {
|
||||
return this.totalCount > 0 ? this.totalCount : ''
|
||||
},
|
||||
product() { return getJoyfulProduct(this.$route.query.id) }
|
||||
imageCount() {
|
||||
return Math.max(this.product.picInfo?.length || 1, 1)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadCart()
|
||||
this.loadProduct()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCartStore, ['loadCart']),
|
||||
async loadProduct() {
|
||||
this.loading = true
|
||||
try {
|
||||
const data = await getFunction('product-detail', { id: this.$route.query.id })
|
||||
this.product = data || emptyProduct
|
||||
} catch (error) {
|
||||
console.warn('查询商品详情失败', error)
|
||||
this.$toast(error.message || '查询商品详情失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
openCartPopup() {
|
||||
this.loadCart()
|
||||
this.showCartPopup = true
|
||||
@@ -130,6 +163,9 @@ export default {
|
||||
this.$router.push({ path: '/joyful-selection/checkout', query: { source: 'cart' } })
|
||||
},
|
||||
openSku(mode) {
|
||||
if (!this.product.id) {
|
||||
return
|
||||
}
|
||||
this.$refs.skuPopup.open(this.product, mode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<van-sku
|
||||
:key="skuKey"
|
||||
v-model="show"
|
||||
:class="`sku-popup--${mode}`"
|
||||
:sku="sku"
|
||||
@@ -46,6 +47,7 @@ export default {
|
||||
data: () => ({
|
||||
show: false,
|
||||
mode: 'cart',
|
||||
skuKey: 0,
|
||||
currentGoods: emptyGoods
|
||||
}),
|
||||
computed: {
|
||||
@@ -103,6 +105,7 @@ export default {
|
||||
open(goods, mode = 'cart') {
|
||||
this.currentGoods = goods
|
||||
this.mode = mode
|
||||
this.skuKey += 1
|
||||
this.show = true
|
||||
},
|
||||
getCartPayload(data) {
|
||||
@@ -110,20 +113,21 @@ export default {
|
||||
const selectedColor = colorOptions.find(item => item.id === selectedSku.s1) || colorOptions[0]
|
||||
const selectedSize = sizeOptions.find(item => item.id === selectedSku.s2) || sizeOptions[0]
|
||||
const count = data.selectedNum || 1
|
||||
const skuId = selectedSku.id || `${this.currentGoods.id}-${selectedColor.id}-${selectedSize.id}`
|
||||
const skuDesc = `${selectedColor.name} / ${selectedSize.name}`
|
||||
|
||||
return {
|
||||
cartId: selectedSku.id || `${this.currentGoods.id}-${selectedColor.id}-${selectedSize.id}`,
|
||||
cartId: skuId,
|
||||
id: this.currentGoods.id,
|
||||
name: this.currentGoods.name,
|
||||
desc: `${selectedColor.name} / ${selectedSize.name}`,
|
||||
desc: skuDesc,
|
||||
productDesc: this.currentGoods.desc || this.currentGoods.detail || '',
|
||||
price: this.currentGoods.price,
|
||||
image: this.currentGoods.image,
|
||||
tag: this.currentGoods.tag,
|
||||
count,
|
||||
sku: {
|
||||
color: selectedColor.name,
|
||||
size: selectedSize.name
|
||||
}
|
||||
sku: skuId,
|
||||
skuText: skuDesc
|
||||
}
|
||||
},
|
||||
addCart(data) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="product-page">
|
||||
<div>
|
||||
<joyful-selection-product-list-skeleton v-if="loading" />
|
||||
<main v-else class="product-page">
|
||||
<div class="fixed-top">
|
||||
<van-nav-bar title="商品列表" left-arrow @click-left="$router.back()" />
|
||||
<div class="search-row">
|
||||
@@ -15,18 +17,23 @@
|
||||
<aside>
|
||||
<div class="layout-aside">
|
||||
<span
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:class="{ active: category.id === selectedCategoryId }"
|
||||
@click="selectedCategoryId = category.id"
|
||||
>
|
||||
{{ category.name }}
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:class="{ active: category.id === selectedCategoryId }"
|
||||
@click="selectedCategoryId = category.id"
|
||||
>
|
||||
{{ category.name }}
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="goods">
|
||||
<article v-for="item in visibleGoods" :key="item.id" class="goods-item" @click="goProductDetail(item)">
|
||||
<article
|
||||
v-for="item in visibleGoods"
|
||||
:key="item.id"
|
||||
class="goods-item"
|
||||
@click="goProductDetail(item)"
|
||||
>
|
||||
<van-image class="goods-thumb" :src="item.image" fit="cover" />
|
||||
<div class="goods-info">
|
||||
<div class="goods-title">
|
||||
@@ -48,35 +55,60 @@
|
||||
</section>
|
||||
|
||||
<joyful-sku-popup ref="skuPopup" />
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { joyfulCategories, joyfulProductList } from '../product-data'
|
||||
import { getFunction } from '@/api/supplied-service'
|
||||
import JoyfulSkuPopup from './components/JoyfulSkuPopup.vue'
|
||||
import JoyfulSelectionProductListSkeleton from '@/skeleton-view/joyful-selection/products/index.vue'
|
||||
|
||||
const defaultCategories = [
|
||||
{ id: 'recommend', name: '推荐' }
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProductList',
|
||||
components: {
|
||||
JoyfulSkuPopup
|
||||
JoyfulSkuPopup,
|
||||
JoyfulSelectionProductListSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
selectedCategoryId: 'recommend',
|
||||
keyword: '',
|
||||
categories: joyfulCategories,
|
||||
goods: joyfulProductList
|
||||
categories: defaultCategories,
|
||||
goods: []
|
||||
}),
|
||||
computed: {
|
||||
visibleGoods() {
|
||||
const categoryGoods = this.selectedCategoryId === 'recommend'
|
||||
? this.goods.slice(0, 10)
|
||||
: this.goods.filter(item => item.categoryId === this.selectedCategoryId)
|
||||
? this.goods
|
||||
: this.goods.filter(item => item.categoryId === this.selectedCategoryId)
|
||||
|
||||
return this.keyword
|
||||
? categoryGoods.filter(item => item.name.includes(this.keyword))
|
||||
: categoryGoods
|
||||
? categoryGoods.filter(item => item.name.includes(this.keyword))
|
||||
: categoryGoods
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadProducts()
|
||||
},
|
||||
methods: {
|
||||
async loadProducts() {
|
||||
this.loading = true
|
||||
try {
|
||||
const data = await getFunction('product-list')
|
||||
this.categories = data?.categories?.length ? data.categories : defaultCategories
|
||||
this.goods = data?.products || []
|
||||
} catch (error) {
|
||||
console.warn('查询商品列表失败', error)
|
||||
this.$toast(error.message || '查询商品列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
goProductDetail(item) {
|
||||
this.$router.push({ path: '/joyful-selection/product-detail', query: { id: item.id } })
|
||||
},
|
||||
@@ -185,9 +217,9 @@ export default {
|
||||
}
|
||||
|
||||
.layout-aside {
|
||||
background-color: #f5f6f8;
|
||||
border-top-right-radius: 15px;
|
||||
overflow: hidden;
|
||||
border-top-right-radius: 15px;
|
||||
background-color: #f5f6f8;
|
||||
}
|
||||
|
||||
.layout aside span {
|
||||
@@ -204,8 +236,8 @@ export default {
|
||||
|
||||
.layout aside .active {
|
||||
position: relative;
|
||||
background: #ededed;;
|
||||
color: #111;
|
||||
background: #ededed;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -340,5 +372,4 @@ export default {
|
||||
.cart-button .van-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,59 +1,345 @@
|
||||
<template>
|
||||
<main class="profile-page">
|
||||
<section class="profile-top">
|
||||
<van-image round width="48" height="48" src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg" />
|
||||
<div><b>张三</b><p>欢迎来到悦享优选</p></div>
|
||||
<van-icon name="ellipsis" />
|
||||
<div>
|
||||
<joyful-selection-profile-skeleton v-if="loading" />
|
||||
<main v-else class="profile-page">
|
||||
<section class="profile-hero">
|
||||
<div class="profile-actions">
|
||||
<van-icon name="setting-o" />
|
||||
<van-icon name="ellipsis" />
|
||||
</div>
|
||||
<div class="user-row">
|
||||
<van-image
|
||||
round
|
||||
width="52"
|
||||
height="52"
|
||||
src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
|
||||
/>
|
||||
<div>
|
||||
<b>张三</b>
|
||||
<p>欢迎来到悦享优选</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="asset">
|
||||
<div><small>我的积分</small><b>2368</b></div>
|
||||
<div><small>成长值</small><b>3680</b></div>
|
||||
<div><small>优惠券</small><b>5</b></div>
|
||||
|
||||
<section class="asset-card">
|
||||
<van-image
|
||||
width="64"
|
||||
height="64"
|
||||
fit="contain"
|
||||
:src="require('@/assets/joyful-selection/profile/coins.png')"
|
||||
/>
|
||||
<div class="asset-info">
|
||||
<small>我的积分</small>
|
||||
<b>{{ formattedPoints }}</b>
|
||||
</div>
|
||||
<button class="points-link" type="button" @click="$router.push('/joyful-selection/points')">
|
||||
<van-image
|
||||
width="18"
|
||||
height="18"
|
||||
fit="contain"
|
||||
:src="require('@/assets/joyful-selection/profile/gift.png')"
|
||||
/>
|
||||
<span>积分明细</span>
|
||||
<van-icon name="arrow" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-title">我的订单 <span @click="$router.push('/joyful-selection/orders')">全部订单 ›</span></div>
|
||||
<van-grid :column-num="5" :border="false">
|
||||
<van-grid-item icon="balance-pay" text="待付款" />
|
||||
<van-grid-item icon="logistics" text="待发货" />
|
||||
<van-grid-item icon="send-gift-o" text="待收货" />
|
||||
<van-grid-item icon="todo-list-o" text="已完成" />
|
||||
<van-grid-item icon="after-sale" text="售后" />
|
||||
</van-grid>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<div class="panel-title">我的服务</div>
|
||||
<div class="panel-title">
|
||||
<b>我的订单</b>
|
||||
<span @click="$router.push('/joyful-selection/orders')">全部订单 <van-icon name="arrow" /></span>
|
||||
</div>
|
||||
<van-grid :column-num="4" :border="false">
|
||||
<van-grid-item icon="star-o" text="我的收藏" />
|
||||
<van-grid-item icon="clock-o" text="浏览记录" />
|
||||
<van-grid-item icon="location-o" text="收货地址" />
|
||||
<van-grid-item icon="coupon-o" text="优惠券" />
|
||||
<van-grid-item icon="gift-o" text="积分商城" @click="$router.push('/joyful-selection/points')" />
|
||||
<van-grid-item icon="service-o" text="客户服务" />
|
||||
<van-grid-item icon="edit" text="意见反馈" />
|
||||
<van-grid-item icon="setting-o" text="设置" />
|
||||
<van-grid-item
|
||||
v-for="item in orderItems"
|
||||
:key="item.text"
|
||||
:icon="item.icon"
|
||||
:text="item.text"
|
||||
:badge="orderBadge(item)"
|
||||
@click="goOrders(item)"
|
||||
/>
|
||||
</van-grid>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<section class="panel service-panel">
|
||||
<div class="panel-title"><b>我的服务</b></div>
|
||||
<div class="coming-soon">
|
||||
<van-image
|
||||
width="104"
|
||||
height="80"
|
||||
fit="contain"
|
||||
src="https://img.yzcdn.cn/vant/empty-image-default.png"
|
||||
/>
|
||||
<p>敬请期待</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import JoyfulSelectionProfileSkeleton from '@/skeleton-view/joyful-selection/profile/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProfile'
|
||||
name: 'JoyfulSelectionProfile',
|
||||
components: {
|
||||
JoyfulSelectionProfileSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
orderItems: [
|
||||
{ icon: 'balance-pay', text: '待付款', tab: 'pending-pay', badgeKey: 'pendingPay' },
|
||||
{ icon: 'logistics', text: '待发货', tab: 'pending-ship' },
|
||||
{ icon: 'send-gift-o', text: '待收货', tab: 'pending-receive', badgeKey: 'pendingReceive' },
|
||||
{ icon: 'after-sale', text: '退款/售后', path: '/joyful-selection/after-sale', badgeKey: 'afterSale' }
|
||||
],
|
||||
orderCounts: {
|
||||
pendingPay: 0,
|
||||
pendingReceive: 0,
|
||||
afterSale: 0
|
||||
}
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['pointsBalance', 'supplierId', 'userId']),
|
||||
formattedPoints() {
|
||||
return Number(this.pointsBalance || 0).toLocaleString('zh-CN', {
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
},
|
||||
orderBadgeCounts() {
|
||||
return this.orderCounts
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loading = true
|
||||
this.fetchProfileData().catch(error => {
|
||||
console.warn('查询用户积分失败', error)
|
||||
this.$toast(error.message || '查询用户积分失败')
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchUserPoints']),
|
||||
async fetchProfileData() {
|
||||
await this.fetchUserPoints()
|
||||
|
||||
if (!this.supplierId || !this.userId) {
|
||||
this.orderCounts = {
|
||||
pendingPay: 0,
|
||||
pendingReceive: 0,
|
||||
afterSale: 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('order-count', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId
|
||||
})
|
||||
|
||||
this.orderCounts = {
|
||||
pendingPay: Number(data?.pendingPay || 0),
|
||||
pendingReceive: Number(data?.pendingReceive || 0),
|
||||
afterSale: Number(data?.afterSale || 0)
|
||||
}
|
||||
},
|
||||
orderBadge(item) {
|
||||
const count = item.badgeKey ? this.orderBadgeCounts[item.badgeKey] : 0
|
||||
|
||||
return count > 0 ? count : undefined
|
||||
},
|
||||
goOrders(item) {
|
||||
if (item.path) {
|
||||
this.$router.push(item.path)
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
path: '/joyful-selection/orders',
|
||||
query: item.tab ? { tab: item.tab } : {}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-page { min-height: 100vh; padding-bottom: 70px; background: #f7f7f7; }
|
||||
.profile-top { display: flex; align-items: center; gap: 12px; padding: 24px 20px; background: linear-gradient(135deg, #dcd8ff, #f9f8ff); }
|
||||
.profile-top div { flex: 1; }
|
||||
.profile-top b { font-size: 17px; }
|
||||
.profile-top p { margin: 4px 0 0; color: #777; font-size: 11px; }
|
||||
.asset { display: flex; margin: -3px 12px 12px; padding: 14px; border-radius: 10px; background: #fff; }
|
||||
.asset div { display: grid; flex: 1; gap: 6px; text-align: center; }
|
||||
.asset small { color: #777; font-size: 11px; }
|
||||
.asset b { color: #ff5b20; }
|
||||
.panel { margin: 10px 12px; border-radius: 10px; background: #fff; }
|
||||
.panel-title { padding: 13px; font-size: 13px; font-weight: bold; }
|
||||
.panel-title span { float: right; color: #999; font-size: 11px; font-weight: normal; }
|
||||
.panel ::v-deep .van-grid-item__text { font-size: 10px; }
|
||||
<style scoped lang="less">
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 92px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.profile-hero {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
min-height: 182px;
|
||||
padding: 26px 18px 56px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-hero::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
background-image: url('~@/assets/joyful-selection/profile-bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
content: '';
|
||||
opacity: .72;
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
gap: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.profile-actions .van-icon {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.user-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-top: 44px;
|
||||
}
|
||||
|
||||
.user-row ::v-deep .van-image {
|
||||
border: 2px solid rgba(255, 255, 255, .72);
|
||||
}
|
||||
|
||||
.user-row b {
|
||||
color: #222;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.user-row p {
|
||||
margin: 6px 0 0;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-height: 92px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: -40px 10px 16px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 16px rgba(61, 68, 92, .06);
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.asset-card small {
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.asset-card b {
|
||||
color: #ff5b20;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.points-link {
|
||||
display: inline-flex;
|
||||
height: 44px;
|
||||
min-width: 118px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: #fff0ec;
|
||||
color: #ff5b20;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin: 16px 10px 0;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
height: 42px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.panel-title b {
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.panel-title span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.panel-title span .van-icon {
|
||||
margin-left: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.panel ::v-deep .van-grid-item__content {
|
||||
padding: 11px 0 16px;
|
||||
}
|
||||
|
||||
.panel ::v-deep .van-grid-item__icon {
|
||||
color: #333;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.panel ::v-deep .van-grid-item__text {
|
||||
margin-top: 6px;
|
||||
color: #333;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.service-panel {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
display: flex;
|
||||
min-height: 196px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0 32px;
|
||||
}
|
||||
|
||||
.coming-soon p {
|
||||
margin: 12px 0 0;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,853 @@
|
||||
<template>
|
||||
<div>
|
||||
<joyful-selection-order-detail-skeleton v-if="loading" />
|
||||
<main v-else class="detail-page">
|
||||
<van-nav-bar title="订单详情" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<van-empty v-if="!currentSubOrder" description="未找到订单" />
|
||||
|
||||
<template v-else>
|
||||
<section class="status-card">
|
||||
<div class="status-card__title">
|
||||
<van-icon name="orders-o" />
|
||||
<div>
|
||||
<h2>{{ currentSubOrder.status }}</h2>
|
||||
<p>{{ statusDesc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-progress">
|
||||
<div class="order-progress__line">
|
||||
<span :style="{ width: progressWidth }"></span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(step, index) in progressSteps"
|
||||
:key="step.text"
|
||||
class="order-progress__step"
|
||||
:class="{ active: index <= activeProgressIndex, passed: index < activeProgressIndex, current: index === activeProgressIndex }"
|
||||
>
|
||||
<i></i>
|
||||
<b>{{ step.text }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="goods-card">
|
||||
<h3 class="card-title">订单信息</h3>
|
||||
<article v-for="item in currentSubOrder.products" :key="item.id" class="goods-item">
|
||||
<van-image class="goods-thumb" :src="item.image" fit="cover" />
|
||||
<div class="goods-info">
|
||||
<h3>{{ item.name }}</h3>
|
||||
<p>{{ skuText(item) }}</p>
|
||||
<b>¥{{ item.price }}</b>
|
||||
</div>
|
||||
<span>x{{ item.number || 1 }}</span>
|
||||
</article>
|
||||
<div v-if="detailActions.length" class="goods-actions">
|
||||
<van-button
|
||||
v-for="action in detailActions"
|
||||
:key="action.key"
|
||||
size="small"
|
||||
:class="action.buttonClass"
|
||||
:loading="isOperating(action.key)"
|
||||
@click="submitDetailAction(action)"
|
||||
>
|
||||
{{ action.text }}
|
||||
</van-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="address-card">
|
||||
<h3 class="card-title">收货地址</h3>
|
||||
<div class="address-row">
|
||||
<div>
|
||||
<p>{{ receiverNameText }} <span>{{ receiverMobileText }}</span></p>
|
||||
<b><van-icon name="location-o" />{{ receiverAddressText }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="remark-row">
|
||||
<span>备注</span>
|
||||
<b>{{ remarkText }}</b>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="info-card">
|
||||
<p><span>订单编号</span><b>{{ currentSubOrder.orderNumberId || currentSubOrder.orderId || '-' }}</b></p>
|
||||
<p><span>下单时间</span><b>{{ currentSubOrder.createTime || currentOrder.createTime || '-' }}</b></p>
|
||||
<p><span>支付方式</span><b>{{ payMethodText }}</b></p>
|
||||
<p v-if="!isPendingPay"><span>支付时间</span><b>{{ payTimeText }}</b></p>
|
||||
<p><span>商品金额</span><b>¥{{ currentSubOrder.amount }}</b></p>
|
||||
<p><span>运费</span><b>¥{{ freightText }}</b></p>
|
||||
<p><span>实付款</span><b class="price">¥{{ paidAmountText }}</b></p>
|
||||
</section>
|
||||
|
||||
<section v-if="showLogisticsCard" class="logistics-card">
|
||||
<h3 class="card-title">物流信息</h3>
|
||||
<div class="logistics-row">
|
||||
<van-icon name="logistics" />
|
||||
<span>{{ logisticsName }}</span>
|
||||
<b>{{ logisticsNo }}</b>
|
||||
<em>{{ logisticsStatusText }}</em>
|
||||
</div>
|
||||
<p>{{ logisticsTimeText }}</p>
|
||||
<p>{{ logisticsDescText }}</p>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { Dialog, Toast } from 'vant'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { savePaymentInfo } from '../../order/payment/payment-data'
|
||||
import JoyfulSelectionOrderDetailSkeleton from '@/skeleton-view/joyful-selection/profile/order-detail/index.vue'
|
||||
|
||||
const ORDER_STATE_VIEW = {
|
||||
0: { text: '待付款', statusClass: 'pending' },
|
||||
1: { text: '待发货', statusClass: 'pending' },
|
||||
2: { text: '待收货', statusClass: 'pending' },
|
||||
3: { text: '已取消', statusClass: 'muted' },
|
||||
5: { text: '已完成', statusClass: 'done' },
|
||||
6: { text: '换货', statusClass: 'muted' },
|
||||
7: { text: '退货', statusClass: 'muted' }
|
||||
}
|
||||
|
||||
const ORDER_EVENT_TYPE = {
|
||||
RETURN_ORDER: 4,
|
||||
CONFIRM_RECEIVE: 6
|
||||
}
|
||||
|
||||
const SKU_COLOR_MAP = {
|
||||
cream: '奶油白',
|
||||
blue: '雾霾蓝',
|
||||
orange: '暖橙色'
|
||||
}
|
||||
|
||||
const SKU_SIZE_MAP = {
|
||||
standard: '标准款',
|
||||
plus: '升级款'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionOrderDetail',
|
||||
components: {
|
||||
JoyfulSelectionOrderDetailSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
operationKey: '',
|
||||
currentOrder: null,
|
||||
currentSubOrder: null
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||
progressSteps() {
|
||||
return [
|
||||
{ text: '待付款', state: 0 },
|
||||
{ text: '待发货', state: 1 },
|
||||
{ text: '待收货', state: 2 },
|
||||
{ text: '已完成', state: 5 }
|
||||
]
|
||||
},
|
||||
activeProgressIndex() {
|
||||
const state = Number(this.currentSubOrder?.orderState)
|
||||
if (state === 0) return 0
|
||||
if (state === 1) return 1
|
||||
if (state === 2) return 2
|
||||
if (state === 5) return 3
|
||||
return 0
|
||||
},
|
||||
progressWidth() {
|
||||
return `${(this.activeProgressIndex / (this.progressSteps.length - 1)) * 100}%`
|
||||
},
|
||||
statusDesc() {
|
||||
const state = Number(this.currentSubOrder?.orderState)
|
||||
if (state === 1) return '商家已发货,等待快递揽收'
|
||||
if (state === 2) return '商品运输中,请留意收货'
|
||||
if (state === 5) return '订单已完成'
|
||||
if (state === 0) return '订单待支付,请及时付款'
|
||||
return '订单状态已更新'
|
||||
},
|
||||
payMethodText() {
|
||||
const payType = Number(this.currentOrder?.wPayOrAPay ?? this.currentSubOrder?.wPayOrAPay ?? 0)
|
||||
return payType === 1 ? '支付宝支付' : '微信支付'
|
||||
},
|
||||
freightText() {
|
||||
return Number(this.currentSubOrder?.freightCost || 0).toFixed(2)
|
||||
},
|
||||
payTimeText() {
|
||||
return this.currentSubOrder?.payTime || this.currentOrder?.payTime || '-'
|
||||
},
|
||||
paidAmountText() {
|
||||
return Number(this.currentSubOrder?.amount || 0).toFixed(2)
|
||||
},
|
||||
receiverNameText() {
|
||||
return this.currentSubOrder?.receiverName || this.currentOrder?.receiverName || '-'
|
||||
},
|
||||
receiverMobileText() {
|
||||
return this.currentSubOrder?.receiverMobile || this.currentOrder?.receiverMobile || '-'
|
||||
},
|
||||
receiverAddressText() {
|
||||
return this.currentSubOrder?.receiverAddress || this.currentOrder?.receiverAddress || '-'
|
||||
},
|
||||
remarkText() {
|
||||
return this.currentSubOrder?.remark || this.currentOrder?.remark || '无'
|
||||
},
|
||||
detailActions() {
|
||||
const state = Number(this.currentSubOrder?.orderState)
|
||||
|
||||
if (state === 0) {
|
||||
return [
|
||||
{ key: 'cancel-main', text: '取消订单', buttonClass: 'dark-action' },
|
||||
{ key: 'pay-main', text: '去支付', buttonClass: 'primary-action' }
|
||||
]
|
||||
}
|
||||
|
||||
if (state === 1 || state === 5) {
|
||||
return [{ key: 'after-sale', orderType: ORDER_EVENT_TYPE.RETURN_ORDER, text: '申请售后', buttonClass: 'dark-action' }]
|
||||
}
|
||||
|
||||
if (state === 2) {
|
||||
return [
|
||||
{ key: 'after-sale', orderType: ORDER_EVENT_TYPE.RETURN_ORDER, text: '申请售后', buttonClass: 'dark-action' },
|
||||
{ key: 'confirm-receive', orderType: ORDER_EVENT_TYPE.CONFIRM_RECEIVE, text: '确认收货', buttonClass: 'primary-action' }
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
isPendingPay() {
|
||||
return Number(this.currentSubOrder?.orderState) === 0
|
||||
},
|
||||
showLogisticsCard() {
|
||||
return Number(this.currentSubOrder?.orderState) > 0
|
||||
},
|
||||
logisticsName() {
|
||||
return this.currentSubOrder?.crrgBsnNm || this.currentOrder?.crrgBsnNm || '申通快递'
|
||||
},
|
||||
logisticsNo() {
|
||||
return this.currentSubOrder?.logisticsOrderId || this.currentOrder?.logisticsOrderId || '-'
|
||||
},
|
||||
logisticsStatusText() {
|
||||
const state = Number(this.currentSubOrder?.orderState)
|
||||
if (state === 1) return '待揽收'
|
||||
if (state === 2) return '运输中'
|
||||
if (state === 5) return '已签收'
|
||||
return '-'
|
||||
},
|
||||
logisticsTimeText() {
|
||||
return this.currentSubOrder?.deliveryTime || this.currentOrder?.deliveryTime || this.currentSubOrder?.createTime || '-'
|
||||
},
|
||||
logisticsDescText() {
|
||||
const state = Number(this.currentSubOrder?.orderState)
|
||||
if (state === 1) return '待商家发货'
|
||||
if (state === 2) return '商家已发货,等待快递揽收'
|
||||
if (state === 5) return '商品已签收,订单已完成'
|
||||
return '物流状态已更新'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadOrderDetail()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
normalizeSubOrder(order, subOrder) {
|
||||
const view = ORDER_STATE_VIEW[Number(subOrder.orderState)] || ORDER_STATE_VIEW[Number(order.orderState)] || ORDER_STATE_VIEW[2]
|
||||
|
||||
return {
|
||||
...subOrder,
|
||||
status: view.text,
|
||||
statusClass: view.statusClass
|
||||
}
|
||||
},
|
||||
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(' | ') : '-'
|
||||
},
|
||||
isOperating(key) {
|
||||
return this.operationKey === key
|
||||
},
|
||||
submitDetailAction(action) {
|
||||
if (action.key === 'cancel-main') {
|
||||
this.cancelMainOrder()
|
||||
return
|
||||
}
|
||||
|
||||
if (action.key === 'pay-main') {
|
||||
this.goPay()
|
||||
return
|
||||
}
|
||||
|
||||
if (action.key === 'after-sale') {
|
||||
this.goAfterSaleSelect()
|
||||
return
|
||||
}
|
||||
|
||||
if (action.key === 'confirm-receive') {
|
||||
this.confirmReceive(action)
|
||||
}
|
||||
},
|
||||
goPay() {
|
||||
const products = this.currentSubOrder?.products || []
|
||||
|
||||
savePaymentInfo({
|
||||
orderNo: this.currentOrder?.mainOrderId || this.currentOrder?.orderId,
|
||||
orderNumberId: this.currentOrder?.orderNumberId || '',
|
||||
amount: Math.round(Number(this.currentOrder?.amount || this.currentSubOrder?.amount || 0) * 100),
|
||||
receiverName: this.receiverNameText === '-' ? '' : this.receiverNameText,
|
||||
receiverTel: this.receiverMobileText === '-' ? '' : this.receiverMobileText,
|
||||
goods: products
|
||||
})
|
||||
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() {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: '取消订单',
|
||||
message: '确定取消该未支付订单吗?'
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = 'cancel-main'
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
const mainOrderId = this.currentOrder?.mainOrderId || this.currentOrder?.orderId
|
||||
if (!this.supplierId || !this.userId || !mainOrderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
await postFunction('order-cancel-main', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
orderId: mainOrderId,
|
||||
mainOrderId
|
||||
})
|
||||
|
||||
Toast('订单已取消')
|
||||
await this.loadOrderDetail()
|
||||
} catch (error) {
|
||||
Toast(error.message || '取消订单失败,请稍后再试')
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
},
|
||||
async submitAfterSale(action) {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: action.text,
|
||||
message: `确定${action.text}吗?提交后将进入商家待审核。`
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = action.key
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
const mainOrderId = this.currentOrder?.mainOrderId || this.currentOrder?.orderId
|
||||
const orderId = this.currentSubOrder?.orderId
|
||||
if (!this.supplierId || !this.userId || !mainOrderId || !orderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
await postFunction('after-sale-apply', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
serviceType: action.orderType,
|
||||
orderId,
|
||||
subOrderId: orderId,
|
||||
mainOrderId,
|
||||
amount: Number(this.currentSubOrder?.amount || 0),
|
||||
reason: action.text
|
||||
})
|
||||
|
||||
Toast(`${action.text}已提交,等待商家审核`)
|
||||
await this.loadOrderDetail()
|
||||
} catch (error) {
|
||||
Toast(error.message || `${action.text}失败,请稍后再试`)
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
},
|
||||
async confirmReceive(action) {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: action.text,
|
||||
message: '确定确认收货吗?'
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = action.key
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
const mainOrderId = this.currentOrder?.mainOrderId || this.currentOrder?.orderId
|
||||
const orderId = this.currentSubOrder?.orderId
|
||||
if (!this.supplierId || !this.userId || !mainOrderId || !orderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
await postFunction('order-push-event', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
orderType: action.orderType,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
subOrderId: orderId,
|
||||
amount: Number(this.currentSubOrder?.amount || 0),
|
||||
reason: action.text,
|
||||
time: Date.now()
|
||||
})
|
||||
|
||||
Toast(`${action.text}成功`)
|
||||
await this.loadOrderDetail()
|
||||
} catch (error) {
|
||||
Toast(error.message || `${action.text}失败,请稍后再试`)
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
},
|
||||
async loadOrderDetail() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId) {
|
||||
Toast('用户信息不完整,请重新进入商城')
|
||||
return
|
||||
}
|
||||
|
||||
const mainOrderId = String(this.$route.query.mainOrderId || '')
|
||||
const orderId = String(this.$route.query.orderId || '')
|
||||
const data = await postFunction('order-detail', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
mainOrderId,
|
||||
orderId
|
||||
})
|
||||
const order = data?.order || null
|
||||
const subOrder = data?.subOrder || null
|
||||
|
||||
this.currentOrder = order || null
|
||||
this.currentSubOrder = order && subOrder ? this.normalizeSubOrder(order, subOrder) : null
|
||||
} catch (error) {
|
||||
Toast(error.message || '查询订单详情失败,请稍后再试')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
padding: 46px 9px 16px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.detail-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);
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.goods-card,
|
||||
.address-card,
|
||||
.info-card,
|
||||
.logistics-card {
|
||||
margin: 10px 0 0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
padding: 14px 14px 16px;
|
||||
border: 1px solid #ffe4d6;
|
||||
background: linear-gradient(135deg, #fff1e6 0%, #fff8f4 100%);
|
||||
}
|
||||
|
||||
.status-card__title {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.status-card__title > .van-icon {
|
||||
display: flex;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 2px;
|
||||
border-radius: 5px;
|
||||
background: #ff6b2c;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.status-card h2 {
|
||||
margin: 0;
|
||||
color: #ff5b20;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.status-card p {
|
||||
margin: 5px 0 0;
|
||||
color: #b69b8b;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.order-progress {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
margin-top: 17px;
|
||||
}
|
||||
|
||||
.order-progress__line {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 38px;
|
||||
left: 38px;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
background: #e9edf0;
|
||||
}
|
||||
|
||||
.order-progress__line span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: #ffb99c;
|
||||
}
|
||||
|
||||
.order-progress__step {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #b7b9be;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.order-progress__step i {
|
||||
position: relative;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: #e7ebee;
|
||||
}
|
||||
|
||||
.order-progress__step.active {
|
||||
color: #ff5b20;
|
||||
}
|
||||
|
||||
.order-progress__step.passed i {
|
||||
background: #ff5b20;
|
||||
}
|
||||
|
||||
.order-progress__step.current {
|
||||
color: #e84b18;
|
||||
}
|
||||
|
||||
.order-progress__step.current i {
|
||||
background: #e84b18;
|
||||
}
|
||||
|
||||
.order-progress__step i::after {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
padding: 15px 15px 14px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 14px;
|
||||
color: #222;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-height: 78px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.goods-item + .goods-item {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f4f4f4;
|
||||
}
|
||||
|
||||
.goods-thumb {
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding: 2px 10px 0 12px;
|
||||
}
|
||||
|
||||
.goods-info h3 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0 0 8px;
|
||||
color: #222;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.goods-info p {
|
||||
overflow: hidden;
|
||||
margin: 0 0 6px;
|
||||
color: #8f8f8f;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-info b,
|
||||
.price {
|
||||
color: #ff5b20;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.goods-item > span {
|
||||
align-self: flex-end;
|
||||
padding-bottom: 2px;
|
||||
color: #111;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.goods-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.goods-actions .van-button {
|
||||
min-width: 88px;
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dark-action {
|
||||
color: #333;
|
||||
background: #f6f7f9;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
color: #ff5b20;
|
||||
background: #fff1e8;
|
||||
}
|
||||
|
||||
.address-card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.address-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.address-row > div {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.address-row p {
|
||||
margin: 0 0 7px;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.address-row p span {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.address-row b {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.address-row b .van-icon {
|
||||
flex: none;
|
||||
margin: 2px 6px 0 0;
|
||||
color: #d58b73;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.remark-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f4f4f4;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.remark-row span {
|
||||
flex: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.remark-row b {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
color: #777;
|
||||
font-size: 13px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.info-card b {
|
||||
max-width: 66%;
|
||||
overflow: hidden;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logistics-card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.logistics-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.logistics-row .van-icon {
|
||||
flex: none;
|
||||
margin-right: 6px;
|
||||
color: #999;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.logistics-row span {
|
||||
flex: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logistics-row b {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 8px;
|
||||
overflow: hidden;
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logistics-row em {
|
||||
flex: none;
|
||||
margin-left: 8px;
|
||||
color: #ff5b20;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logistics-card p {
|
||||
margin: 5px 0 0 21px;
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +1,796 @@
|
||||
<template>
|
||||
<main class="orders-page">
|
||||
<van-nav-bar title="我的订单" left-arrow @click-left="$router.back()" />
|
||||
<van-tabs v-model="active">
|
||||
<van-tab v-for="tab in tabs" :key="tab" :title="tab" />
|
||||
</van-tabs>
|
||||
<section>
|
||||
<article v-for="order in orders" :key="order.id">
|
||||
<header><span>订单号:{{ order.id }}</span><b :class="order.status">{{ order.status }}</b></header>
|
||||
<van-card
|
||||
:title="order.product.name"
|
||||
desc="共 3 件商品"
|
||||
price="297.90"
|
||||
:thumb="order.product.image"
|
||||
/>
|
||||
<footer><span>合计:<b>¥297.90</b></span><van-button size="small" round plain type="danger">查看详情</van-button></footer>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
<div>
|
||||
<joyful-selection-orders-skeleton v-if="loading" />
|
||||
<main v-else class="orders-page">
|
||||
<van-nav-bar title="我的订单" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
|
||||
<van-tabs
|
||||
v-model="active"
|
||||
class="order-tabs"
|
||||
line-width="18"
|
||||
line-height="2"
|
||||
color="#ff5b20"
|
||||
title-active-color="#ff5b20"
|
||||
>
|
||||
<van-tab v-for="tab in tabs" :key="tab.label" :title="tab.label" :badge="tabBadge(tab)" />
|
||||
</van-tabs>
|
||||
|
||||
<van-pull-refresh v-model="refreshing" class="order-list" @refresh="onRefresh">
|
||||
<van-empty v-if="visibleOrders.length === 0" description="暂无订单" />
|
||||
|
||||
<template v-else>
|
||||
<article v-for="order in visibleOrders" :key="order.viewId" class="order-card">
|
||||
<header class="order-card__header">
|
||||
<div class="order-card__no">
|
||||
<van-icon name="orders-o" />
|
||||
<span>订单号:{{ order.orderNumberId || order.orderId || '-' }}</span>
|
||||
</div>
|
||||
<b :class="order.statusClass">{{ order.status }}</b>
|
||||
</header>
|
||||
|
||||
<div class="order-card__body" @click="goOrderDetail(order)">
|
||||
<div class="order-goods-list">
|
||||
<div
|
||||
v-for="item in previewProducts(order.products)"
|
||||
:key="item.id"
|
||||
class="order-goods-row"
|
||||
>
|
||||
<van-image
|
||||
class="order-thumb"
|
||||
:src="item.image"
|
||||
fit="cover"
|
||||
/>
|
||||
<div class="order-goods__info">
|
||||
<h3>{{ item.name || '-' }}</h3>
|
||||
<p>{{ skuText(item) }}</p>
|
||||
<b>¥{{ item.price || order.amount }}</b>
|
||||
</div>
|
||||
<span class="order-goods-count">x{{ productCount(item) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showLogistics(order)" class="logistics-row">
|
||||
<van-icon name="logistics" />
|
||||
<span>物流状态:包裹已到达目的地城市</span>
|
||||
<van-icon name="arrow" />
|
||||
</div>
|
||||
|
||||
<div class="order-card__summary">
|
||||
<span>共{{ orderProductCount(order) }}件商品</span>
|
||||
<span>合计:<b>¥{{ order.amount }}</b></span>
|
||||
</div>
|
||||
|
||||
<p class="order-create-time">下单时间:{{ order.createTime || '-' }}</p>
|
||||
|
||||
<footer v-if="orderActions(order).length">
|
||||
<div class="order-actions">
|
||||
<van-button
|
||||
v-for="action in orderActions(order)"
|
||||
:key="action.key"
|
||||
size="small"
|
||||
round
|
||||
:class="action.buttonClass"
|
||||
:loading="isOperating(action.key, action.orderId)"
|
||||
@click.stop="submitOrderAction(order, action)"
|
||||
>
|
||||
{{ action.text }}
|
||||
</van-button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
</van-pull-refresh>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { joyfulProducts } from '../../product-data'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { Dialog, Toast } from 'vant'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useUiStore } from '@/store/ui'
|
||||
import { savePaymentInfo } from '../../order/payment/payment-data'
|
||||
import JoyfulSelectionOrdersSkeleton from '@/skeleton-view/joyful-selection/profile/orders/index.vue'
|
||||
|
||||
const ORDER_STATE = {
|
||||
PENDING_PAY: 0,
|
||||
FINISH_PAY: 1,
|
||||
IN_PROGRESS: 2,
|
||||
CANCELLED: 3,
|
||||
COMPLETED: 5,
|
||||
EXCHANGE: 6,
|
||||
REFUNDED: 7
|
||||
}
|
||||
|
||||
const ORDER_EVENT_TYPE = {
|
||||
RETURN_ORDER: 4,
|
||||
EXCHANGE_ORDER: 5,
|
||||
CONFIRM_RECEIVE: 6,
|
||||
INVOICE_INFO: 11
|
||||
}
|
||||
|
||||
const ORDER_STATE_VIEW = {
|
||||
[ORDER_STATE.PENDING_PAY]: { text: '待付款', statusClass: 'pending' },
|
||||
[ORDER_STATE.FINISH_PAY]: { text: '待发货', statusClass: 'pending' },
|
||||
[ORDER_STATE.IN_PROGRESS]: { text: '待收货', statusClass: 'pending' },
|
||||
[ORDER_STATE.CANCELLED]: { text: '已取消', statusClass: 'muted' },
|
||||
[ORDER_STATE.COMPLETED]: { text: '已完成', statusClass: 'done' },
|
||||
[ORDER_STATE.EXCHANGE]: { text: '换货', statusClass: 'muted' },
|
||||
[ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted' }
|
||||
}
|
||||
|
||||
const SKU_COLOR_MAP = {
|
||||
cream: '奶油白',
|
||||
blue: '雾霾蓝',
|
||||
orange: '暖橙色'
|
||||
}
|
||||
|
||||
const SKU_SIZE_MAP = {
|
||||
standard: '标准款',
|
||||
plus: '升级款'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionOrders',
|
||||
components: {
|
||||
JoyfulSelectionOrdersSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
active: 0,
|
||||
tabs: ['全部', '待付款', '待发货', '待收货', '已完成', '售后'],
|
||||
orders: [
|
||||
{ id: 'YX2024051812345678', status: '待发货', product: joyfulProducts[0] },
|
||||
{ id: 'YX2024051509876543', status: '待收货', product: joyfulProducts[1] },
|
||||
{ id: 'YX2024051211223344', status: '已完成', product: joyfulProducts[2] }
|
||||
]
|
||||
})
|
||||
tabs: [
|
||||
{ label: '全部', type: 'all' },
|
||||
{ label: '待付款', type: 'pending-main' },
|
||||
{ label: '待发货', type: 'sub-state', state: ORDER_STATE.FINISH_PAY },
|
||||
{ label: '待收货', type: 'sub-state', state: ORDER_STATE.IN_PROGRESS },
|
||||
{ label: '已取消', type: 'cancelled' },
|
||||
{ label: '已完成', type: 'sub-state', state: ORDER_STATE.COMPLETED }
|
||||
],
|
||||
orderItems: [],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
operationKey: ''
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||
...mapState(useUiStore, ['ordersTab']),
|
||||
active: {
|
||||
get() {
|
||||
return this.ordersTab
|
||||
},
|
||||
set(value) {
|
||||
this.setOrdersTab(value)
|
||||
}
|
||||
},
|
||||
activeTab() {
|
||||
return this.tabs[this.active] || this.tabs[0]
|
||||
},
|
||||
visibleOrders() {
|
||||
const tab = this.activeTab
|
||||
|
||||
if (tab.type === 'all') {
|
||||
return this.orderItems
|
||||
}
|
||||
|
||||
if (tab.type === 'pending-main') {
|
||||
return this.orderItems.filter(order => order.displayType === 'main' && Number(order.orderState) === ORDER_STATE.PENDING_PAY)
|
||||
}
|
||||
|
||||
if (tab.type === 'sub-state') {
|
||||
return this.orderItems.filter(order => order.displayType === 'sub' && Number(order.orderState) === tab.state)
|
||||
}
|
||||
|
||||
if (tab.type === 'cancelled') {
|
||||
return this.orderItems.filter(order => Number(order.orderState) === ORDER_STATE.CANCELLED)
|
||||
}
|
||||
|
||||
if (tab.type === 'after-sale') {
|
||||
return this.orderItems.filter(order => {
|
||||
return order.displayType === 'sub' && [ORDER_STATE.EXCHANGE, ORDER_STATE.REFUNDED].includes(Number(order.orderState))
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.applyRouteTab()
|
||||
this.loadOrders()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
...mapActions(useUiStore, ['loadUi', 'setOrdersTab']),
|
||||
applyRouteTab() {
|
||||
this.loadUi()
|
||||
const tabIndexMap = {
|
||||
'pending-pay': 1,
|
||||
'pending-ship': 2,
|
||||
'pending-receive': 3,
|
||||
completed: 5
|
||||
}
|
||||
const nextActive = tabIndexMap[this.$route.query.tab]
|
||||
|
||||
if (nextActive !== undefined) {
|
||||
this.setOrdersTab(nextActive)
|
||||
}
|
||||
},
|
||||
previewProducts(products = []) {
|
||||
return products.slice(0, 3)
|
||||
},
|
||||
firstProduct(order) {
|
||||
return order.products?.[0] || {}
|
||||
},
|
||||
productCount(product = {}) {
|
||||
return Number(product.number || product.count || 1)
|
||||
},
|
||||
orderProductCount(order) {
|
||||
const products = Array.isArray(order.products) ? order.products : []
|
||||
const count = products.reduce((total, product) => total + this.productCount(product), 0)
|
||||
|
||||
return count || Number(order.count || 0)
|
||||
},
|
||||
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(' | ') : '-'
|
||||
},
|
||||
showLogistics(order) {
|
||||
return ![ORDER_STATE.PENDING_PAY, ORDER_STATE.CANCELLED, ORDER_STATE.COMPLETED].includes(Number(order.orderState))
|
||||
},
|
||||
actionKey(type, orderId) {
|
||||
return `${type}-${orderId}`
|
||||
},
|
||||
isOperating(type, orderId) {
|
||||
return this.operationKey === this.actionKey(type, orderId)
|
||||
},
|
||||
tabBadge(tab) {
|
||||
if (tab.type !== 'pending-main') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const count = this.orderItems.filter(order => {
|
||||
return order.displayType === 'main' && Number(order.orderState) === ORDER_STATE.PENDING_PAY
|
||||
}).length
|
||||
|
||||
return count > 0 ? count : undefined
|
||||
},
|
||||
subActions(subOrder) {
|
||||
const state = Number(subOrder.orderState)
|
||||
const actions = []
|
||||
|
||||
if ([ORDER_STATE.FINISH_PAY, ORDER_STATE.IN_PROGRESS, ORDER_STATE.COMPLETED].includes(state)) {
|
||||
actions.push({ orderType: ORDER_EVENT_TYPE.RETURN_ORDER, text: '申请售后', buttonClass: 'dark-action' })
|
||||
}
|
||||
|
||||
if (state === ORDER_STATE.IN_PROGRESS) {
|
||||
actions.push({ orderType: ORDER_EVENT_TYPE.CONFIRM_RECEIVE, text: '确认收货', buttonClass: 'primary-action' })
|
||||
}
|
||||
|
||||
return actions
|
||||
},
|
||||
goOrderDetail(order) {
|
||||
const subOrder = order.displayType === 'main' ? order.subOrders?.[0] : order
|
||||
|
||||
if (!subOrder?.orderId) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
path: '/joyful-selection/order-detail',
|
||||
query: {
|
||||
mainOrderId: order.mainOrderId,
|
||||
orderId: subOrder.orderId
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadOrders(showLoading = true) {
|
||||
if (showLoading) {
|
||||
this.loading = true
|
||||
}
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId) {
|
||||
Toast('用户信息不完整,请重新进入商城')
|
||||
this.orderItems = []
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('order-list', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId
|
||||
})
|
||||
|
||||
this.orderItems = data?.orderItems || []
|
||||
} catch (error) {
|
||||
Toast(error.message || '查询订单失败,请稍后再试')
|
||||
this.orderItems = []
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
async onRefresh() {
|
||||
try {
|
||||
await this.loadOrders(false)
|
||||
} finally {
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
orderActions(order) {
|
||||
if (order.displayType === 'main') {
|
||||
return Number(order.orderState) === ORDER_STATE.PENDING_PAY
|
||||
? [
|
||||
{ key: 'cancel-main', orderId: order.mainOrderId, text: '取消订单', buttonClass: 'dark-action' },
|
||||
{ key: 'pay-main', orderId: order.mainOrderId, text: '去支付', buttonClass: 'primary-action' }
|
||||
]
|
||||
: []
|
||||
}
|
||||
|
||||
return this.subActions(order).map(action => ({
|
||||
...action,
|
||||
key: action.key || action.orderType,
|
||||
orderId: order.orderId
|
||||
}))
|
||||
},
|
||||
submitOrderAction(order, action) {
|
||||
if (action.key === 'cancel-main') {
|
||||
this.cancelMainOrder(order)
|
||||
return
|
||||
}
|
||||
|
||||
if (action.key === 'ship-order') {
|
||||
this.shipOrder(order)
|
||||
return
|
||||
}
|
||||
|
||||
if (action.key === 'pay-main') {
|
||||
this.goPay(order)
|
||||
return
|
||||
}
|
||||
|
||||
if ([ORDER_EVENT_TYPE.RETURN_ORDER, ORDER_EVENT_TYPE.EXCHANGE_ORDER].includes(action.orderType)) {
|
||||
this.goAfterSaleSelect(order)
|
||||
return
|
||||
}
|
||||
|
||||
this.submitSubOrderEvent(
|
||||
{ mainOrderId: order.mainOrderId },
|
||||
order,
|
||||
action
|
||||
)
|
||||
},
|
||||
goPay(order) {
|
||||
const firstProduct = this.firstProduct(order)
|
||||
|
||||
savePaymentInfo({
|
||||
orderNo: order.mainOrderId || order.orderId,
|
||||
orderNumberId: order.orderNumberId || '',
|
||||
amount: Math.round(Number(order.amount || 0) * 100),
|
||||
receiverName: order.receiverName || '',
|
||||
receiverTel: order.receiverMobile || '',
|
||||
goods: order.products || (firstProduct.id ? [firstProduct] : [])
|
||||
})
|
||||
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) {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: '取消订单',
|
||||
message: '确定取消该未支付订单吗?'
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = this.actionKey('cancel-main', order.mainOrderId)
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId || !order.mainOrderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
await postFunction('order-cancel-main', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
orderId: order.mainOrderId,
|
||||
mainOrderId: order.mainOrderId
|
||||
})
|
||||
|
||||
Toast('订单已取消')
|
||||
await this.loadOrders()
|
||||
} catch (error) {
|
||||
Toast(error.message || '取消订单失败,请稍后再试')
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
},
|
||||
async shipOrder(order) {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: '发货',
|
||||
message: '确定将该订单更新为已发货吗?'
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = this.actionKey('ship-order', order.orderId)
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId || !order.orderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
await postFunction('order-ship', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
orderId: order.orderId,
|
||||
mainOrderId: order.mainOrderId,
|
||||
deliveryStatus: 1
|
||||
})
|
||||
|
||||
Toast('发货成功')
|
||||
await this.loadOrders()
|
||||
} catch (error) {
|
||||
Toast(error.message || '发货失败,请稍后再试')
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
},
|
||||
async submitAfterSale(order, action) {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: action.text,
|
||||
message: `确定申请${action.text}吗?提交后将进入商家待审核。`
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = this.actionKey(action.orderType, order.orderId)
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId || !order.mainOrderId || !order.orderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
await postFunction('after-sale-apply', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
serviceType: action.orderType,
|
||||
orderId: order.orderId,
|
||||
subOrderId: order.orderId,
|
||||
mainOrderId: order.mainOrderId,
|
||||
amount: Number(order.amount || 0),
|
||||
reason: action.text
|
||||
})
|
||||
|
||||
Toast(`${action.text}申请已提交,等待商家审核`)
|
||||
await this.loadOrders()
|
||||
} catch (error) {
|
||||
Toast(error.message || `${action.text}申请失败,请稍后再试`)
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
},
|
||||
async submitSubOrderEvent(order, subOrder, action) {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: action.text,
|
||||
message: `确定对该子单进行“${action.text}”操作吗?`
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = this.actionKey(action.orderType, subOrder.orderId)
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId || !order.mainOrderId || !subOrder.orderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('order-push-event', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
orderType: action.orderType,
|
||||
orderId: subOrder.orderId,
|
||||
mainOrderId: order.mainOrderId,
|
||||
subOrderId: subOrder.orderId,
|
||||
amount: Number(subOrder.amount || 0),
|
||||
reason: action.text,
|
||||
invoiceBizId: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? `INV_${subOrder.orderId}` : undefined,
|
||||
invoiceIds: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? `INV_${subOrder.orderId}` : undefined,
|
||||
orderIds: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? subOrder.orderId : undefined,
|
||||
time: Date.now()
|
||||
})
|
||||
|
||||
Toast(data?.pushSuccess === false ? '本地已保存,远程推送失败' : `${action.text}成功`)
|
||||
await this.loadOrders()
|
||||
} catch (error) {
|
||||
Toast(error.message || `${action.text}失败,请稍后再试`)
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.orders-page { min-height: 100vh; padding-top: 46px; background: #f7f7f7; }
|
||||
.orders-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); }
|
||||
.orders-page section { padding: 0 12px; }
|
||||
.orders-page article { margin-top: 10px; border-radius: 10px; background: #fff; }
|
||||
.orders-page header, .orders-page footer { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; color: #999; font-size: 11px; }
|
||||
.orders-page header b { color: #ff5b20; }
|
||||
.orders-page .已完成 { color: #27ae60; }
|
||||
.orders-page footer b { color: #222; }
|
||||
.orders-page .van-card { background: #fff; }
|
||||
<style scoped lang="less">
|
||||
.orders-page {
|
||||
min-height: 100vh;
|
||||
padding-top: 90px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.orders-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);
|
||||
}
|
||||
|
||||
.order-tabs {
|
||||
position: fixed;
|
||||
z-index: 19;
|
||||
top: 46px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.orders-page ::v-deep .van-tabs__wrap {
|
||||
height: 44px;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
|
||||
}
|
||||
|
||||
.orders-page ::v-deep .van-tab {
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
min-height: calc(100vh - 90px);
|
||||
box-sizing: border-box;
|
||||
padding: 12px 10px 18px;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
overflow: hidden;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.order-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 14px 8px;
|
||||
}
|
||||
|
||||
.order-card__no {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.order-card__no .van-icon {
|
||||
flex: none;
|
||||
margin-right: 8px;
|
||||
color: #ff5b20;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.order-card__no span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #222;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-card__header b {
|
||||
flex: none;
|
||||
margin-left: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-card__header .pending {
|
||||
color: #ff5b20;
|
||||
}
|
||||
|
||||
.order-card__header .done {
|
||||
color: #21b36b;
|
||||
}
|
||||
|
||||
.order-card__header .muted {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-card__body {
|
||||
padding: 12px 14px 14px;
|
||||
}
|
||||
|
||||
.order-goods-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-goods-row {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.order-thumb {
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.order-goods__info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding: 2px 10px 0 12px;
|
||||
}
|
||||
|
||||
.order-goods__info h3 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0 0 8px;
|
||||
color: #222;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 21px;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.order-goods__info p {
|
||||
overflow: hidden;
|
||||
margin: 4px 0 0;
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-goods__info b {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
color: #ff5b20;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-goods-count {
|
||||
flex: none;
|
||||
align-self: flex-end;
|
||||
padding-bottom: 4px;
|
||||
color: #222;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.logistics-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 14px;
|
||||
padding: 13px 0;
|
||||
border-top: 1px dashed #eee;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
color: #777;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.logistics-row .van-icon {
|
||||
flex: none;
|
||||
color: #777;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.logistics-row span {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-card__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px 0;
|
||||
color: #555;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.order-card__summary b {
|
||||
color: #ff5b20;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-create-time {
|
||||
margin: 6px 14px 14px;
|
||||
color: #aaa;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.order-card footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 14px 16px;
|
||||
}
|
||||
|
||||
.order-actions {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.order-actions .van-button {
|
||||
min-width: 88px;
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
color: #ff5b20;
|
||||
background: #fff1e8;
|
||||
}
|
||||
|
||||
.dark-action {
|
||||
color: #333;
|
||||
background: #f6f7f9;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,149 @@
|
||||
<template><main class="points-page"><van-nav-bar title="我的积分" left-arrow @click-left="$router.back()"/><section class="score"><small>可用积分</small><b>2368</b><van-button size="small" round @click="$router.push('/joyful-selection/products')">积分说明</van-button></section><van-grid :column-num="4" :border="false"><van-grid-item icon="gift-o" text="积分商城"/><van-grid-item icon="gift-card-o" text="积分抽奖"/><van-grid-item icon="balance-list-o" text="积分兑换"/><van-grid-item icon="todo-list-o" text="积分签到"/></van-grid><section class="details"><h3>积分明细</h3><van-cell v-for="item in items" :key="item.title" :title="item.title" :label="item.time" :value="item.value" :value-class="item.value[0] === '+' ? 'income' : 'cost'"/><p>没有更多了~</p></section></main></template>
|
||||
<script>export default{name:'JoyfulSelectionPoints',data:()=>({items:[{title:'签到奖励',time:'2024-05-18 09:30',value:'+10'},{title:'订单完成奖励',time:'2024-05-14 10:20',value:'+88'},{title:'积分兑换',time:'2024-05-08 15:11',value:'-200'},{title:'晒单奖励',time:'2024-05-02 08:30',value:'+20'},{title:'评价奖励',time:'2024-04-28 12:08',value:'+5'}]})}</script>
|
||||
<style scoped>.points-page{min-height:100vh;padding-top:46px;background:#f7f7f7}.points-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)}.score{position:relative;margin:10px 12px;padding:16px 18px;border-radius:10px;background:linear-gradient(135deg,#ffe0ac,#ffc66e)}.score small,.score b{display:block}.score small{font-size:11px}.score b{margin-top:6px;font-size:22px}.score .van-button{position:absolute;right:14px;top:24px;background:rgba(255,255,255,.6)}.points-page>.van-grid{margin:0 12px;border-radius:10px;background:#fff}.details{margin:10px 12px;padding:2px 0;border-radius:10px;background:#fff}.details h3{margin:14px;font-size:14px}.details p{text-align:center;color:#aaa;font-size:11px}.income{color:#14a44d}.cost{color:#f15b45}</style>
|
||||
<template>
|
||||
<div>
|
||||
<joyful-selection-points-skeleton v-if="loading" />
|
||||
<main v-else class="points-page">
|
||||
<van-nav-bar title="我的积分" left-arrow @click-left="$router.back()" />
|
||||
<section class="score">
|
||||
<small>可用积分</small>
|
||||
<b>{{ formattedPoints }}</b>
|
||||
<van-button size="small" round @click="$router.push('/joyful-selection/products')">积分说明</van-button>
|
||||
</section>
|
||||
<van-grid :column-num="4" :border="false">
|
||||
<van-grid-item icon="gift-o" text="积分商城" />
|
||||
<van-grid-item icon="gift-card-o" text="积分抽奖" />
|
||||
<van-grid-item icon="balance-list-o" text="积分兑换" />
|
||||
<van-grid-item icon="todo-list-o" text="积分签到" />
|
||||
</van-grid>
|
||||
<section class="details">
|
||||
<h3>积分明细</h3>
|
||||
<van-cell
|
||||
v-for="item in items"
|
||||
:key="item.title"
|
||||
:title="item.title"
|
||||
:label="item.time"
|
||||
:value="item.value"
|
||||
:value-class="item.value[0] === '+' ? 'income' : 'cost'"
|
||||
/>
|
||||
<p>没有更多了~</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import JoyfulSelectionPointsSkeleton from '@/skeleton-view/joyful-selection/profile/points/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPoints',
|
||||
components: {
|
||||
JoyfulSelectionPointsSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
items: [
|
||||
{ title: '订单兑换', time: '2024-05-18 14:35', value: '-287.9' },
|
||||
{ title: '积分发放', time: '2024-05-10 09:20', value: '+300' }
|
||||
]
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['pointsBalance']),
|
||||
formattedPoints() {
|
||||
return Number(this.pointsBalance || 0).toLocaleString('zh-CN', {
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loading = true
|
||||
this.fetchUserPoints().catch(error => {
|
||||
console.warn('查询用户积分失败', error)
|
||||
this.$toast(error.message || '查询用户积分失败')
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchUserPoints'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.points-page {
|
||||
min-height: 100vh;
|
||||
padding-top: 46px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.points-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);
|
||||
}
|
||||
|
||||
.score {
|
||||
position: relative;
|
||||
margin: 10px 12px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #ffe0ac, #ffc66e);
|
||||
}
|
||||
|
||||
.score small,
|
||||
.score b {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.score small {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.score b {
|
||||
margin-top: 6px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.score .van-button {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 14px;
|
||||
background: rgba(255, 255, 255, .6);
|
||||
}
|
||||
|
||||
.points-page > .van-grid {
|
||||
margin: 0 12px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin: 10px 12px;
|
||||
padding: 2px 0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.details h3 {
|
||||
margin: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.details p {
|
||||
color: #aaa;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.income {
|
||||
color: #14a44d;
|
||||
}
|
||||
|
||||
.cost {
|
||||
color: #f15b45;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<main class="supplier-logs-page">
|
||||
<van-nav-bar title="供应商日志" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<section class="toolbar">
|
||||
<span :class="{ active: listening }">{{ listening ? '监听中' : '已暂停' }}</span>
|
||||
<button type="button" @click="loadLogs">刷新</button>
|
||||
<button type="button" @click="clearLogs">清除</button>
|
||||
</section>
|
||||
|
||||
<section class="console-panel">
|
||||
<div v-if="consoleLogs.length === 0" class="console-empty">暂无日志,正在监听请求...</div>
|
||||
<article v-for="item in consoleLogs" :key="item.id" class="console-line" :class="item.type">
|
||||
<p><span>{{ formatTime(item.time) }}</span><b>{{ item.title }}:</b></p>
|
||||
<pre>{{ formatRequestLine(item) }}</pre>
|
||||
<pre>请求参数:{{ formatRequestParams(item.content) }}</pre>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Toast } from 'vant'
|
||||
import { getFunction } from '@/api/supplied-service'
|
||||
|
||||
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/api'
|
||||
const LOG_GROUPS = [
|
||||
{ type: 'backend-receive', title: '当前后端接收请求', emptyText: '暂无后端接收请求' },
|
||||
{ type: 'zhgh-send', title: '向ZHGH发送请求', emptyText: '暂无向ZHGH发送请求' },
|
||||
{ type: 'zhgh-receive', title: '接收ZHGH请求', emptyText: '暂无接收ZHGH请求' }
|
||||
]
|
||||
const POLL_INTERVAL = 2000
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionSupplierLogs',
|
||||
data: () => ({
|
||||
logs: [],
|
||||
timer: null,
|
||||
listening: false
|
||||
}),
|
||||
computed: {
|
||||
consoleLogs() {
|
||||
return this.logs
|
||||
.filter(item => LOG_GROUPS.some(group => group.type === item.type))
|
||||
.slice()
|
||||
.reverse()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadLogs()
|
||||
this.startListening()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopListening()
|
||||
},
|
||||
methods: {
|
||||
async loadLogs() {
|
||||
try {
|
||||
const data = await getFunction('supplier-logs')
|
||||
this.logs = data?.logs || []
|
||||
} catch (error) {
|
||||
Toast(error.message || '查询日志失败')
|
||||
}
|
||||
},
|
||||
async clearLogs() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE.replace(/\/$/, '')}/supplier-logs`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok || payload.success === false) {
|
||||
throw new Error(payload.error?.message || payload.msg || '清除日志失败')
|
||||
}
|
||||
this.logs = []
|
||||
Toast('日志已清除')
|
||||
} catch (error) {
|
||||
Toast(error.message || '清除日志失败')
|
||||
}
|
||||
},
|
||||
startListening() {
|
||||
this.stopListening()
|
||||
this.listening = true
|
||||
this.timer = window.setInterval(() => {
|
||||
this.loadLogs()
|
||||
}, POLL_INTERVAL)
|
||||
},
|
||||
stopListening() {
|
||||
if (this.timer) {
|
||||
window.clearInterval(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
this.listening = false
|
||||
},
|
||||
formatTime(time) {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return time
|
||||
const pad = value => String(value).padStart(2, '0')
|
||||
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
},
|
||||
formatRequestLine(item) {
|
||||
const content = item.content || {}
|
||||
const method = content.method || 'POST'
|
||||
const url = this.fullUrl(content.url || content.path || '-')
|
||||
|
||||
return `${method} ${url}`
|
||||
},
|
||||
fullUrl(url) {
|
||||
if (!url || url === '-') return '-'
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
const base = API_BASE.replace(/\/api\/?$/, '').replace(/\/$/, '')
|
||||
const path = String(url).startsWith('/') ? url : `/${url}`
|
||||
|
||||
return `${base}${path}`
|
||||
},
|
||||
formatRequestParams(content = {}) {
|
||||
const params = {
|
||||
...(content.query || {}),
|
||||
...(content.body || {})
|
||||
}
|
||||
const entries = Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||
|
||||
if (entries.length === 0) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return entries.map(([key, value]) => `${key}=${this.formatParamValue(value)}`).join('&')
|
||||
},
|
||||
formatParamValue(value) {
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.supplier-logs-page {
|
||||
height: 100vh;
|
||||
padding: 56px 10px 18px;
|
||||
overflow: hidden;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.supplier-logs-page ::v-deep .van-nav-bar {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toolbar span {
|
||||
position: relative;
|
||||
padding-left: 14px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toolbar span::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #bbb;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.toolbar span.active {
|
||||
color: #20b879;
|
||||
}
|
||||
|
||||
.toolbar span.active::before {
|
||||
background: #20b879;
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #ff5b20;
|
||||
border-radius: 6px;
|
||||
color: #ff5b20;
|
||||
font-size: 13px;
|
||||
background: #fff7f2;
|
||||
}
|
||||
|
||||
.console-panel {
|
||||
height: calc(100vh - 116px);
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.console-empty {
|
||||
color: #7fda89;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.console-line + .console-line {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.console-line p {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
color: #7fda89;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.console-line span {
|
||||
color: #8aa0b4;
|
||||
}
|
||||
|
||||
.console-line b {
|
||||
color: #ffd166;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.console-line.backend-receive b {
|
||||
color: #7fda89;
|
||||
}
|
||||
|
||||
.console-line.zhgh-send b {
|
||||
color: #5cc8ff;
|
||||
}
|
||||
|
||||
.console-line.zhgh-receive b {
|
||||
color: #ff8a5c;
|
||||
}
|
||||
|
||||
.console-line pre {
|
||||
overflow-x: auto;
|
||||
margin: 4px 0 0;
|
||||
color: #e6edf3;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,24 @@
|
||||
# MySQL connection string for Koa runtime and Prisma CLI.
|
||||
DATABASE_URL="mysql://supper:Supper%40123456@localhost:10086/supplier_db"
|
||||
PRISMA_CLIENT_ENGINE_TYPE="binary"
|
||||
|
||||
# Optional app settings.
|
||||
JWT_SECRET="replace-me"
|
||||
UPLOAD_TOKEN="replace-me"
|
||||
|
||||
# Main points mall bridge API, for example http://localhost/open.
|
||||
POINTS_MALL_OPEN_API_BASE="http://localhost/open"
|
||||
SUPPLIER_OPEN_SM4_KEY_HEX="00112233445566778899aabbccddeeff"
|
||||
SUPPLIER_OPEN_SM4_IV_HEX="0102030405060708090a0b0c0d0e0f10"
|
||||
|
||||
# Redis token cache. Redis DB 9 is used for platform bridge tokens.
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_PASSWORD=""
|
||||
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"
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
.netlify/
|
||||
.env
|
||||
dist/
|
||||
coverage/
|
||||
*.log
|
||||
@@ -0,0 +1,61 @@
|
||||
# supplied-service
|
||||
|
||||
Netlify Functions backend for the supplied mall H5 project.
|
||||
|
||||
## Stack
|
||||
|
||||
- Netlify Functions
|
||||
- TypeScript
|
||||
- Prisma ORM
|
||||
- Neon Postgres
|
||||
- `@prisma/adapter-neon` + `@neondatabase/serverless`
|
||||
|
||||
## Directory
|
||||
|
||||
```text
|
||||
netlify/
|
||||
functions/ # API entrypoints
|
||||
utils/ # shared function utilities
|
||||
types/ # shared API types
|
||||
prisma/
|
||||
schema.prisma # Prisma models
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
Copy `.env.example` to `.env` and set:
|
||||
|
||||
- `DATABASE_URL`: Neon Postgres connection string for Netlify Functions runtime and Prisma CLI
|
||||
- `JWT_SECRET`: auth signing secret
|
||||
- `UPLOAD_TOKEN`: upload guard token
|
||||
- `POINTS_MALL_OPEN_API_BASE`: main platform bridge base URL, for example `http://localhost/open`
|
||||
|
||||
Supplier H5 uses `VUE_APP_SUPPLIER_IDENTIFICATION` to request supplier config from this service. The supplier `clientSecret` stays in the database and is only used by the Netlify Function when requesting the platform bridge token.
|
||||
|
||||
## Scripts
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
npm run build
|
||||
npm run typecheck
|
||||
npm run prisma:generate
|
||||
npm run prisma:migrate
|
||||
npm run prisma:studio
|
||||
```
|
||||
|
||||
## Function Routes
|
||||
|
||||
Netlify exposes functions under `/.netlify/functions/<name>`.
|
||||
|
||||
Current placeholders:
|
||||
|
||||
- `login`
|
||||
- `product-list`
|
||||
- `product-detail`
|
||||
- `cart-add`
|
||||
- `order-create`
|
||||
- `order-pay`
|
||||
- `user-info`
|
||||
- `upload`
|
||||
- `supplier-info`
|
||||
- `supplier-token`
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "supplied-service",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx src/server.ts",
|
||||
"start": "tsx src/server.ts",
|
||||
"build": "prisma generate",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:studio": "prisma studio",
|
||||
"seed:products": "node prisma/seed-products.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@prisma/adapter-neon": "6.19.3",
|
||||
"@prisma/client": "6.19.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"ioredis": "^5.4.2",
|
||||
"@koa/router": "^13.1.1",
|
||||
"koa": "^2.15.3",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/koa": "^2.15.0",
|
||||
"@types/koa-bodyparser": "^4.3.12",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"prisma": "6.19.3",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'dotenv/config'
|
||||
import { defineConfig, env } from 'prisma/config'
|
||||
|
||||
export default defineConfig({
|
||||
schema: 'prisma/schema.prisma',
|
||||
datasource: {
|
||||
url: env('DATABASE_URL')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,368 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
mobile String @unique
|
||||
nickname String?
|
||||
avatarUrl String?
|
||||
points Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
carts CartItem[]
|
||||
}
|
||||
|
||||
model SupplierProduct {
|
||||
id Int @id @default(autoincrement())
|
||||
sku String @unique
|
||||
number Int @default(0)
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
name String
|
||||
taxRate Decimal @default(0.13) @db.Decimal(5, 4)
|
||||
picInfo Json?
|
||||
description String?
|
||||
detail String?
|
||||
originPrice Decimal? @db.Decimal(10, 2)
|
||||
imageUrl String?
|
||||
categoryId String?
|
||||
category String?
|
||||
tag String?
|
||||
sales String?
|
||||
stock Int @default(0)
|
||||
sortOrder Int @default(0)
|
||||
enabled Boolean @default(true)
|
||||
deleted Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
carts CartItem[]
|
||||
|
||||
@@map("supplier_product")
|
||||
}
|
||||
|
||||
model SupplierInfo {
|
||||
id Int @id @default(autoincrement())
|
||||
supplierIdentification String @unique
|
||||
supplierId String @unique
|
||||
supplierName String
|
||||
clientId String
|
||||
clientSecret String
|
||||
supplierApiUrl String?
|
||||
sroNo String?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
creator String?
|
||||
updator String?
|
||||
version Int @default(0)
|
||||
deleted Int @default(0)
|
||||
|
||||
@@map("supplier_info")
|
||||
}
|
||||
|
||||
model CartItem {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
productId Int
|
||||
skuText String?
|
||||
quantity Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
product SupplierProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, productId, skuText])
|
||||
}
|
||||
|
||||
model SupplierOrderMain {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId String @unique
|
||||
orderNumberId String?
|
||||
supplierId String
|
||||
userId String?
|
||||
aplName String?
|
||||
addressId String?
|
||||
remark String? @db.Text
|
||||
createTime DateTime?
|
||||
updateTime DateTime?
|
||||
payTime DateTime?
|
||||
deliveryTime DateTime?
|
||||
orderFinishTime DateTime?
|
||||
completeTime DateTime?
|
||||
deliveryStatus String?
|
||||
freightCost Decimal @default(0) @db.Decimal(10, 2)
|
||||
wPayOrAPayFreightCost Decimal @default(0) @db.Decimal(10, 2)
|
||||
refund Decimal @default(0) @db.Decimal(10, 2)
|
||||
wPayOrAPayRefund Decimal @default(0) @db.Decimal(10, 2)
|
||||
logisticsOrderId String?
|
||||
crrgBsnNm String?
|
||||
orderState Int @default(0)
|
||||
isSplit Int @default(0)
|
||||
splitTime DateTime?
|
||||
totalPrice Decimal @default(0) @db.Decimal(10, 2)
|
||||
wPayOrAPay Decimal @default(0) @db.Decimal(10, 2)
|
||||
pointsPrice Decimal @default(0) @db.Decimal(10, 2)
|
||||
invoiceCode String?
|
||||
createAt DateTime @default(now())
|
||||
updateAt DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
subOrders SupplierOrderSub[]
|
||||
|
||||
@@map("supplier_order_main")
|
||||
}
|
||||
|
||||
model SupplierOrderSub {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId String @unique
|
||||
orderNumberId String?
|
||||
mainOrderId String
|
||||
supplierId String
|
||||
token String?
|
||||
userId String?
|
||||
aplName String?
|
||||
addressId String?
|
||||
remark String? @db.Text
|
||||
createTime DateTime?
|
||||
updateTime DateTime?
|
||||
payTime DateTime?
|
||||
deliveryTime DateTime?
|
||||
orderFinishTime DateTime?
|
||||
completeTime DateTime?
|
||||
deliveryStatus String?
|
||||
freightCost Decimal @default(0) @db.Decimal(10, 2)
|
||||
wPayOrAPayFreightCost Decimal @default(0) @db.Decimal(10, 2)
|
||||
refund Decimal @default(0) @db.Decimal(10, 2)
|
||||
wPayOrAPayRefund Decimal @default(0) @db.Decimal(10, 2)
|
||||
logisticsOrderId String?
|
||||
crrgBsnNm String?
|
||||
orderState Int @default(0)
|
||||
totalPrice Decimal @default(0) @db.Decimal(10, 2)
|
||||
wPayOrAPay Decimal @default(0) @db.Decimal(10, 2)
|
||||
pointsPrice Decimal @default(0) @db.Decimal(10, 2)
|
||||
invoiceCode String?
|
||||
createAt DateTime @default(now())
|
||||
updateAt DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
mainOrder SupplierOrderMain @relation(fields: [mainOrderId], references: [orderId], onDelete: Cascade)
|
||||
products SupplierOrderProduct[]
|
||||
|
||||
@@index([mainOrderId])
|
||||
@@map("supplier_order_sub")
|
||||
}
|
||||
|
||||
model SupplierOrderProduct {
|
||||
id Int @id @default(autoincrement())
|
||||
subOrderId String
|
||||
sku String
|
||||
number Int
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
name String
|
||||
taxRate Decimal @default(0.13) @db.Decimal(5, 4)
|
||||
productImgUrl String?
|
||||
fileId String?
|
||||
filePath String?
|
||||
picInfo Json?
|
||||
createAt DateTime @default(now())
|
||||
updateAt DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
subOrder SupplierOrderSub @relation(fields: [subOrderId], references: [orderId], onDelete: Cascade)
|
||||
|
||||
@@index([subOrderId])
|
||||
@@map("supplier_order_product")
|
||||
}
|
||||
|
||||
model SupplierUserAddress {
|
||||
id Int @id @default(autoincrement())
|
||||
addressId String @unique
|
||||
supplierId String
|
||||
userId String
|
||||
name String
|
||||
tel String
|
||||
province String?
|
||||
city String?
|
||||
county String?
|
||||
addressDetail String? @db.Text
|
||||
areaCode String?
|
||||
postalCode String?
|
||||
isDefault Boolean @default(false)
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, userId])
|
||||
@@index([supplierId, userId, isDefault])
|
||||
@@map("supplier_user_address")
|
||||
}
|
||||
|
||||
model SupplierPointsLedger {
|
||||
id Int @id @default(autoincrement())
|
||||
ledgerNo String @unique
|
||||
supplierId String
|
||||
userId String
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
bizType Int
|
||||
direction Int
|
||||
points Decimal @db.Decimal(10, 2)
|
||||
beforePoints Decimal? @db.Decimal(10, 2)
|
||||
afterPoints Decimal? @db.Decimal(10, 2)
|
||||
remark String? @db.Text
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, userId])
|
||||
@@index([mainOrderId])
|
||||
@@index([subOrderId])
|
||||
@@map("supplier_points_ledger")
|
||||
}
|
||||
|
||||
model SupplierOrderPushLog {
|
||||
id Int @id @default(autoincrement())
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
orderId String? @db.VarChar(1000)
|
||||
orderType Int?
|
||||
apiPath String
|
||||
requestBody Json?
|
||||
responseBody Json?
|
||||
httpStatus Int?
|
||||
resultCode Int?
|
||||
pushStatus Int @default(0)
|
||||
retryCount Int @default(0)
|
||||
errorMessage String? @db.Text
|
||||
lastPushTime DateTime?
|
||||
nextRetryTime DateTime?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([orderType, pushStatus])
|
||||
@@index([nextRetryTime])
|
||||
@@map("supplier_order_push_log")
|
||||
}
|
||||
|
||||
model SupplierOrderOperateLog {
|
||||
id Int @id @default(autoincrement())
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
operateType Int
|
||||
beforeState Int?
|
||||
afterState Int?
|
||||
beforeSplit Int?
|
||||
afterSplit Int?
|
||||
remark String? @db.Text
|
||||
operator String?
|
||||
createTime DateTime @default(now())
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([subOrderId])
|
||||
@@index([operateType])
|
||||
@@map("supplier_order_operate_log")
|
||||
}
|
||||
|
||||
model SupplierRefundExchange {
|
||||
id Int @id @default(autoincrement())
|
||||
serviceOrderId String @unique
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
serviceType Int
|
||||
serviceState Int @default(0)
|
||||
amount Decimal @default(0) @db.Decimal(10, 2)
|
||||
reason String? @db.Text
|
||||
requestBody Json?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([subOrderId])
|
||||
@@index([serviceType, serviceState])
|
||||
@@map("supplier_refund_exchange")
|
||||
}
|
||||
|
||||
model SupplierAfterSale {
|
||||
id Int @id @default(autoincrement())
|
||||
serviceOrderId String @unique
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
serviceType Int
|
||||
serviceState Int @default(0)
|
||||
auditStatus Int @default(0)
|
||||
logisticsState Int @default(0)
|
||||
amount Decimal @default(0) @db.Decimal(10, 2)
|
||||
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
|
||||
userLogisticsNo String?
|
||||
userLogisticsCompany String?
|
||||
merchantLogisticsNo String?
|
||||
merchantLogisticsCompany String?
|
||||
applyTime DateTime @default(now())
|
||||
auditTime DateTime?
|
||||
userShipTime DateTime?
|
||||
merchantReceiveTime DateTime?
|
||||
merchantShipTime DateTime?
|
||||
finishTime DateTime?
|
||||
requestBody Json?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([supplierId, userId])
|
||||
@@index([subOrderId])
|
||||
@@index([serviceType, serviceState])
|
||||
@@index([auditStatus, logisticsState])
|
||||
@@map("supplier_after_sale")
|
||||
}
|
||||
|
||||
model SupplierInvoiceInfo {
|
||||
id Int @id @default(autoincrement())
|
||||
invoiceBizId String @unique
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
settlementId String?
|
||||
invoiceCode String?
|
||||
invoiceIds String? @db.VarChar(1000)
|
||||
orderIds String? @db.VarChar(1000)
|
||||
invoiceStatus Int @default(0)
|
||||
fileId String?
|
||||
filePath String?
|
||||
requestBody Json?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([settlementId])
|
||||
@@index([invoiceCode])
|
||||
@@map("supplier_invoice_info")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const imagePool = [
|
||||
'https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1',
|
||||
'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1',
|
||||
'https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1',
|
||||
'https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3',
|
||||
'https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3'
|
||||
]
|
||||
|
||||
const categories = {
|
||||
homeLiving: '居家生活',
|
||||
digitalAppliance: '数码家电',
|
||||
beautyCare: '美妆个护',
|
||||
foodDrink: '食品饮料',
|
||||
motherBaby: '母婴玩具',
|
||||
fashionBag: '服饰鞋包',
|
||||
sportsOutdoor: '运动户外',
|
||||
recommend: '精选推荐'
|
||||
}
|
||||
|
||||
const rawProducts = [
|
||||
['SKU001', '蓝牙耳机', '轻盈佩戴,稳定连接,通勤运动都好用', 149.99, 199.99, 'digitalAppliance', '积分兑换', '1.2万件', 2],
|
||||
['SKU002', '手机壳', '防滑耐磨,贴合机身,日常保护更安心', 10.01, 19.9, 'digitalAppliance', '赠品', '5632件', 1],
|
||||
['SKU003', '智能音箱 Pro', '语音唤醒,沉浸音效,适合卧室和书房', 99.0, 139.0, 'digitalAppliance', '热门推荐', '2.1万件', 1],
|
||||
['SKU004', '奶油白迷你烤箱', '小巧不占地,早餐烘焙好帮手', 129.0, 169.0, 'homeLiving', '新品上架', '1.2万件', 1],
|
||||
['SKU005', '北欧陶瓷餐具套装', '简约釉色,日常用餐更有仪式感', 69.9, 99.9, 'homeLiving', '品质精选', '6453件', 1],
|
||||
['SKU006', '纯棉舒适四件套', '柔软亲肤,安心睡眠,四季可用', 179.0, 229.0, 'homeLiving', '积分兑换', '8321件', 1],
|
||||
['SKU007', '植萃保湿护肤礼盒', '补水保湿,清爽不黏腻', 99.9, 139.9, 'beautyCare', '礼盒精选', '4820件', 1],
|
||||
['SKU008', '香氛洗护旅行套装', '温和清洁,清新留香,出行便携', 59.9, 89.9, 'beautyCare', '新品上架', '7932件', 1],
|
||||
['SKU009', '便携补光化妆镜', '高清镜面,柔光补妆,随身携带', 49.9, 79.9, 'beautyCare', '热门推荐', '3651件', 1],
|
||||
['SKU010', '橙意零食礼盒', '精选零食组合,办公室分享刚刚好', 59.9, 89.9, 'foodDrink', '礼盒精选', '6324件', 1],
|
||||
['SKU011', '咖啡随行杯组合', '锁温防漏,通勤随行更方便', 49.9, 69.9, 'foodDrink', '积分兑换', '3520件', 1],
|
||||
['SKU012', '下午茶点心礼盒', '多种风味,一盒满足甜咸口味', 69.9, 99.9, 'foodDrink', '品质精选', '4891件', 1],
|
||||
['SKU013', '安心儿童水杯', '轻巧易握,防摔耐用,出门好携带', 39.9, 59.9, 'motherBaby', '新品上架', '4250件', 1],
|
||||
['SKU014', '益智积木套装', '色彩柔和,锻炼动手能力', 89.0, 129.0, 'motherBaby', '热门推荐', '2186件', 1],
|
||||
['SKU015', '柔光儿童夜灯', '温和光线,陪伴睡前时光', 69.9, 99.9, 'motherBaby', '品质精选', '3548件', 1],
|
||||
['SKU016', '大容量电脑背包', '分区收纳,通勤出行更轻松', 149.0, 199.0, 'fashionBag', '积分兑换', '1.6万件', 1],
|
||||
['SKU017', '轻巧通勤斜挎包', '简洁百搭,日常出门更实用', 79.9, 109.9, 'fashionBag', '新品上架', '5321件', 1],
|
||||
['SKU018', '柔软撞色丝巾礼盒', '细腻亲肤,点亮日常穿搭', 69.9, 99.9, 'fashionBag', '礼盒精选', '2680件', 1],
|
||||
['SKU019', '运动保温水杯', '长效锁温,户外运动随行', 49.9, 79.9, 'sportsOutdoor', '热门推荐', '7342件', 1],
|
||||
['SKU020', '筋膜放松按摩仪 Mini', '轻巧便携,缓解久坐酸胀', 99.0, 149.0, 'sportsOutdoor', '积分兑换', '3921件', 1],
|
||||
['SKU021', '户外露营灯', '多档照明,露营夜晚更安心', 79.0, 119.0, 'sportsOutdoor', '新品上架', '2568件', 1],
|
||||
['SKU022', '悦享精选礼包', '多场景精选好物,福利兑换优选', 59.9, 89.9, 'recommend', '精选推荐', '8820件', 1],
|
||||
['SKU023', '品质生活组合装', '居家、通勤、日用好物组合', 119.0, 159.0, 'recommend', '热门推荐', '6902件', 1],
|
||||
['SKU024', '积分兑换惊喜盒', '随机精选商品,兑换更有新鲜感', 39.9, 69.9, 'recommend', '限时秒杀', '5310件', 1]
|
||||
]
|
||||
|
||||
const products = rawProducts.map(([sku, name, description, price, originPrice, categoryId, tag, sales, number], index) => {
|
||||
const imageUrl = imagePool[index % imagePool.length]
|
||||
const nextImageUrl = imagePool[(index + 1) % imagePool.length]
|
||||
|
||||
return {
|
||||
sku,
|
||||
number,
|
||||
price,
|
||||
name,
|
||||
taxRate: 0.13,
|
||||
picInfo: [imageUrl, nextImageUrl],
|
||||
description,
|
||||
detail: `${description}。精选品质商品,适合积分商城兑换和日常福利采购场景。`,
|
||||
originPrice,
|
||||
imageUrl,
|
||||
categoryId,
|
||||
category: categories[categoryId],
|
||||
tag,
|
||||
sales,
|
||||
stock: 180 + index * 7,
|
||||
sortOrder: index + 1,
|
||||
enabled: true,
|
||||
deleted: 0
|
||||
}
|
||||
})
|
||||
|
||||
const main = async () => {
|
||||
for (const product of products) {
|
||||
await prisma.supplierProduct.upsert({
|
||||
where: { sku: product.sku },
|
||||
update: product,
|
||||
create: product
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect()
|
||||
console.log(`已导入 ${products.length} 条商品数据`)
|
||||
})
|
||||
.catch(async error => {
|
||||
console.error(error)
|
||||
await prisma.$disconnect()
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_points_ledger` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '积分流水主键ID',
|
||||
`ledgerNo` VARCHAR(191) NOT NULL COMMENT '流水号,业务唯一',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NOT NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||
`bizType` INT NOT NULL COMMENT '业务类型:1支付消费,2取消返还,3退货退款,4人工调整',
|
||||
`direction` INT NOT NULL COMMENT '流水方向:1增加,2扣减',
|
||||
`points` DECIMAL(10, 2) NOT NULL COMMENT '积分变动金额',
|
||||
`beforePoints` DECIMAL(10, 2) NULL COMMENT '变动前积分余额',
|
||||
`afterPoints` DECIMAL(10, 2) NULL COMMENT '变动后积分余额',
|
||||
`remark` TEXT NULL COMMENT '流水备注',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_points_ledger_ledgerNo_key` (`ledgerNo`),
|
||||
KEY `supplier_points_ledger_supplierId_userId_idx` (`supplierId`, `userId`),
|
||||
KEY `supplier_points_ledger_mainOrderId_idx` (`mainOrderId`),
|
||||
KEY `supplier_points_ledger_subOrderId_idx` (`subOrderId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商用户积分消费流水表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_order_push_log` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单推送日志主键ID',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '主单ID',
|
||||
`orderId` VARCHAR(1000) NULL COMMENT '推送订单ID,多个子单用英文逗号分隔',
|
||||
`orderType` INT NULL COMMENT '推送事件类型:1取消主单,2拆单子单,3取消子单,7取消已支付未拆单整单',
|
||||
`apiPath` VARCHAR(191) NOT NULL COMMENT '推送接口路径',
|
||||
`requestBody` JSON NULL COMMENT '推送请求体',
|
||||
`responseBody` JSON NULL COMMENT '推送响应体',
|
||||
`httpStatus` INT NULL COMMENT 'HTTP状态码',
|
||||
`resultCode` INT NULL COMMENT '远程业务返回码',
|
||||
`pushStatus` INT NOT NULL DEFAULT 0 COMMENT '推送状态:0待推送,1推送成功,2推送失败',
|
||||
`retryCount` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
|
||||
`errorMessage` TEXT NULL COMMENT '失败原因',
|
||||
`lastPushTime` DATETIME(3) NULL COMMENT '最后推送时间',
|
||||
`nextRetryTime` DATETIME(3) NULL COMMENT '下次重试时间',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `supplier_order_push_log_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_order_push_log_orderType_pushStatus_idx` (`orderType`, `pushStatus`),
|
||||
KEY `supplier_order_push_log_nextRetryTime_idx` (`nextRetryTime`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商订单远程推送日志表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_order_operate_log` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单操作日志主键ID',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '子单ID',
|
||||
`operateType` INT NOT NULL COMMENT '操作类型:1创建待支付,2支付成功,3拆单,4取消主单,5取消子单,6取消已支付未拆单整单,7已妥投,8已完成,9退款,10换货',
|
||||
`beforeState` INT NULL COMMENT '操作前订单状态',
|
||||
`afterState` INT NULL COMMENT '操作后订单状态',
|
||||
`beforeSplit` INT NULL COMMENT '操作前拆单状态:0未拆单,1已拆单',
|
||||
`afterSplit` INT NULL COMMENT '操作后拆单状态:0未拆单,1已拆单',
|
||||
`remark` TEXT NULL COMMENT '操作备注',
|
||||
`operator` VARCHAR(191) NULL COMMENT '操作人或系统标识',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `supplier_order_operate_log_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_order_operate_log_subOrderId_idx` (`subOrderId`),
|
||||
KEY `supplier_order_operate_log_operateType_idx` (`operateType`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商订单操作日志表';
|
||||
@@ -0,0 +1,56 @@
|
||||
SET @fk_order_item_order = (
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'orderitem'
|
||||
AND REFERENCED_TABLE_NAME = 'order'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SET @sql = IF(
|
||||
@fk_order_item_order IS NULL,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `orderitem` DROP FOREIGN KEY `', @fk_order_item_order, '`')
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @fk_order_item_product = (
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'orderitem'
|
||||
AND REFERENCED_TABLE_NAME = 'supplier_product'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SET @sql = IF(
|
||||
@fk_order_item_product IS NULL,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `orderitem` DROP FOREIGN KEY `', @fk_order_item_product, '`')
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @fk_order_user = (
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'order'
|
||||
AND REFERENCED_TABLE_NAME = 'User'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SET @sql = IF(
|
||||
@fk_order_user IS NULL,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `order` DROP FOREIGN KEY `', @fk_order_user, '`')
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
DROP TABLE IF EXISTS `orderitem`;
|
||||
DROP TABLE IF EXISTS `order`;
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE `supplier_order_main`
|
||||
ADD COLUMN `addressId` VARCHAR(191) NULL COMMENT '收货地址ID' AFTER `aplName`,
|
||||
ADD COLUMN `remark` TEXT NULL COMMENT '订单备注' AFTER `addressId`;
|
||||
|
||||
ALTER TABLE `supplier_order_sub`
|
||||
ADD COLUMN `addressId` VARCHAR(191) NULL COMMENT '收货地址ID' AFTER `aplName`,
|
||||
ADD COLUMN `remark` TEXT NULL COMMENT '订单备注' AFTER `addressId`;
|
||||
@@ -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_order_main`
|
||||
MODIFY COLUMN `orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1已支付,2进行中,3已取消,4已妥投,5已完成,6换货,7退货,8废除',
|
||||
ADD COLUMN `isSplit` TINYINT NOT NULL DEFAULT 0 COMMENT '主单是否拆单:0未拆单,1已拆单' AFTER `orderState`,
|
||||
ADD COLUMN `splitTime` DATETIME(3) NULL COMMENT '主单拆单时间' AFTER `isSplit`;
|
||||
|
||||
ALTER TABLE `supplier_order_sub`
|
||||
MODIFY COLUMN `orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1已支付,2进行中,3已取消,4已妥投,5已完成,6换货,7退货,8废除';
|
||||
@@ -0,0 +1,115 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_product` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '商品主键ID',
|
||||
`sku` VARCHAR(191) NOT NULL COMMENT '商品SKU编码',
|
||||
`number` INT NOT NULL DEFAULT 0 COMMENT '默认购买数量或导入数量',
|
||||
`price` DECIMAL(10, 2) NOT NULL COMMENT '商品销售价格',
|
||||
`name` VARCHAR(191) NOT NULL COMMENT '商品名称',
|
||||
`taxRate` DECIMAL(5, 4) NOT NULL DEFAULT 0.1300 COMMENT '商品税率',
|
||||
`picInfo` JSON NULL COMMENT '商品图片列表,JSON数组',
|
||||
`description` VARCHAR(191) NULL COMMENT '商品简短描述',
|
||||
`detail` VARCHAR(191) NULL COMMENT '商品详情描述',
|
||||
`originPrice` DECIMAL(10, 2) NULL COMMENT '商品原价',
|
||||
`imageUrl` VARCHAR(191) NULL COMMENT '商品主图地址',
|
||||
`categoryId` VARCHAR(191) NULL COMMENT '商品分类ID',
|
||||
`category` VARCHAR(191) NULL COMMENT '商品分类名称',
|
||||
`tag` VARCHAR(191) NULL COMMENT '商品标签',
|
||||
`sales` VARCHAR(191) NULL COMMENT '商品销量展示文案',
|
||||
`stock` INT NOT NULL DEFAULT 0 COMMENT '商品库存',
|
||||
`sortOrder` INT NOT NULL DEFAULT 0 COMMENT '排序值,越小越靠前',
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT true COMMENT '是否启用',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updatedAt` DATETIME(3) NOT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_product_sku_key` (`sku`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商商品信息表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_order_main` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主单主键ID',
|
||||
`orderId` VARCHAR(191) NOT NULL COMMENT '主订单ID',
|
||||
`orderNumberId` VARCHAR(191) NULL COMMENT '主订单编号',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '平台用户ID',
|
||||
`aplName` VARCHAR(191) NULL COMMENT '应用名称',
|
||||
`createTime` DATETIME(3) NULL COMMENT '订单创建时间',
|
||||
`updateTime` DATETIME(3) NULL COMMENT '订单更新时间',
|
||||
`deliveryTime` DATETIME(3) NULL COMMENT '发货时间',
|
||||
`orderFinishTime` DATETIME(3) NULL COMMENT '订单完成时间',
|
||||
`completeTime` DATETIME(3) NULL COMMENT '交易完成时间',
|
||||
`deliveryStatus` VARCHAR(191) NULL COMMENT '配送状态',
|
||||
`freightCost` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '运费积分金额',
|
||||
`wPayOrAPayFreightCost` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付运费金额',
|
||||
`refund` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分退款金额',
|
||||
`wPayOrAPayRefund` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝退款金额',
|
||||
`logisticsOrderId` VARCHAR(191) NULL COMMENT '物流单号',
|
||||
`crrgBsnNm` VARCHAR(191) NULL COMMENT '承运商名称',
|
||||
`orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态',
|
||||
`totalPrice` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额',
|
||||
`wPayOrAPay` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付金额',
|
||||
`pointsPrice` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分支付金额',
|
||||
`invoiceCode` VARCHAR(191) NULL COMMENT '发票编码',
|
||||
`createAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
|
||||
`updateAt` DATETIME(3) NOT NULL COMMENT '记录更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_order_main_orderId_key` (`orderId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商主订单表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_order_sub` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '子单主键ID',
|
||||
`orderId` VARCHAR(191) NOT NULL COMMENT '子订单ID',
|
||||
`orderNumberId` VARCHAR(191) NULL COMMENT '子订单编号',
|
||||
`mainOrderId` VARCHAR(191) NOT NULL COMMENT '所属主订单ID',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`token` VARCHAR(191) NULL COMMENT '平台桥接访问令牌快照',
|
||||
`userId` VARCHAR(191) NULL COMMENT '平台用户ID',
|
||||
`aplName` VARCHAR(191) NULL COMMENT '应用名称',
|
||||
`createTime` DATETIME(3) NULL COMMENT '订单创建时间',
|
||||
`updateTime` DATETIME(3) NULL COMMENT '订单更新时间',
|
||||
`deliveryTime` DATETIME(3) NULL COMMENT '发货时间',
|
||||
`orderFinishTime` DATETIME(3) NULL COMMENT '订单完成时间',
|
||||
`completeTime` DATETIME(3) NULL COMMENT '交易完成时间',
|
||||
`deliveryStatus` VARCHAR(191) NULL COMMENT '配送状态',
|
||||
`freightCost` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '运费积分金额',
|
||||
`wPayOrAPayFreightCost` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付运费金额',
|
||||
`refund` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分退款金额',
|
||||
`wPayOrAPayRefund` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝退款金额',
|
||||
`logisticsOrderId` VARCHAR(191) NULL COMMENT '物流单号',
|
||||
`crrgBsnNm` VARCHAR(191) NULL COMMENT '承运商名称',
|
||||
`orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态',
|
||||
`totalPrice` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '子订单总金额',
|
||||
`wPayOrAPay` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付金额',
|
||||
`pointsPrice` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分支付金额',
|
||||
`invoiceCode` VARCHAR(191) NULL COMMENT '发票编码',
|
||||
`createAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
|
||||
`updateAt` DATETIME(3) NOT NULL COMMENT '记录更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_order_sub_orderId_key` (`orderId`),
|
||||
KEY `supplier_order_sub_mainOrderId_idx` (`mainOrderId`),
|
||||
CONSTRAINT `supplier_order_sub_mainOrderId_fkey`
|
||||
FOREIGN KEY (`mainOrderId`) REFERENCES `supplier_order_main` (`orderId`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商子订单表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_order_product` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '子单商品主键ID',
|
||||
`subOrderId` VARCHAR(191) NOT NULL COMMENT '所属子订单ID',
|
||||
`sku` VARCHAR(191) NOT NULL COMMENT '商品SKU编码',
|
||||
`number` INT NOT NULL COMMENT '商品购买数量',
|
||||
`price` DECIMAL(10, 2) NOT NULL COMMENT '商品成交单价',
|
||||
`name` VARCHAR(191) NOT NULL COMMENT '商品名称快照',
|
||||
`taxRate` DECIMAL(5, 4) NOT NULL DEFAULT 0.1300 COMMENT '商品税率快照',
|
||||
`productImgUrl` VARCHAR(191) NULL COMMENT '商品图片地址',
|
||||
`fileId` VARCHAR(191) NULL COMMENT '平台文件ID或图片ID',
|
||||
`filePath` VARCHAR(191) NULL COMMENT '文件存储路径',
|
||||
`picInfo` JSON NULL COMMENT '商品图片列表快照,JSON数组',
|
||||
`createAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
|
||||
`updateAt` DATETIME(3) NOT NULL COMMENT '记录更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `supplier_order_product_subOrderId_idx` (`subOrderId`),
|
||||
CONSTRAINT `supplier_order_product_subOrderId_fkey`
|
||||
FOREIGN KEY (`subOrderId`) REFERENCES `supplier_order_sub` (`orderId`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商子订单商品表';
|
||||
@@ -0,0 +1,46 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_refund_exchange` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '售后记录主键ID',
|
||||
`serviceOrderId` VARCHAR(191) NOT NULL COMMENT '退货/换货业务单号',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||
`serviceType` INT NOT NULL COMMENT '售后类型:4退货,5换货',
|
||||
`serviceState` INT NOT NULL DEFAULT 0 COMMENT '售后状态:0已创建,1处理中,2已完成,3已取消',
|
||||
`amount` DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '售后金额',
|
||||
`reason` TEXT NULL COMMENT '售后原因',
|
||||
`requestBody` JSON NULL COMMENT '原始业务请求体',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_refund_exchange_serviceOrderId_key` (`serviceOrderId`),
|
||||
KEY `supplier_refund_exchange_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_refund_exchange_subOrderId_idx` (`subOrderId`),
|
||||
KEY `supplier_refund_exchange_serviceType_serviceState_idx` (`serviceType`, `serviceState`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商退货换货售后记录表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_invoice_info` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '发票信息主键ID',
|
||||
`invoiceBizId` VARCHAR(191) NOT NULL COMMENT '发票业务唯一ID,优先使用发票号或事件订单号',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||
`settlementId` VARCHAR(191) NULL COMMENT '结算单号',
|
||||
`invoiceCode` VARCHAR(191) NULL COMMENT '发票代码或纳税识别号',
|
||||
`invoiceIds` VARCHAR(1000) NULL COMMENT '发票号码列表,多个用英文逗号分隔',
|
||||
`orderIds` VARCHAR(1000) NULL COMMENT '关联订单号列表,多个用英文逗号分隔',
|
||||
`invoiceStatus` INT NOT NULL DEFAULT 0 COMMENT '发票状态:0已记录,1已申请,2已开票,3失败',
|
||||
`fileId` VARCHAR(191) NULL COMMENT '发票文件ID',
|
||||
`filePath` VARCHAR(191) NULL COMMENT '发票文件路径',
|
||||
`requestBody` JSON NULL COMMENT '原始业务请求体',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_invoice_info_invoiceBizId_key` (`invoiceBizId`),
|
||||
KEY `supplier_invoice_info_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_invoice_info_settlementId_idx` (`settlementId`),
|
||||
KEY `supplier_invoice_info_invoiceCode_idx` (`invoiceCode`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商发票信息表';
|
||||
@@ -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;
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_after_sale` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '供应商售后单主键ID',
|
||||
`serviceOrderId` VARCHAR(191) NOT NULL COMMENT '售后业务单号',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||
`serviceType` INT NOT NULL COMMENT '售后类型:4退货,5换货',
|
||||
`serviceState` INT NOT NULL DEFAULT 0 COMMENT '售后状态:0商家待审核,1审核通过,2审核拒绝,3用户已寄回,4商家已收货,5已完成,6已取消',
|
||||
`auditStatus` INT NOT NULL DEFAULT 0 COMMENT '审核状态:0待审核,1审核通过,2审核拒绝',
|
||||
`logisticsState` INT NOT NULL DEFAULT 0 COMMENT '物流状态:0无物流,1待用户寄回,2用户已寄回,3商家已收货,4商家已发货,5用户已收货',
|
||||
`amount` DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '售后金额',
|
||||
`reason` TEXT NULL COMMENT '售后原因',
|
||||
`auditRemark` TEXT NULL COMMENT '审核备注',
|
||||
`userLogisticsNo` VARCHAR(191) NULL COMMENT '用户寄回物流单号',
|
||||
`userLogisticsCompany` VARCHAR(191) NULL COMMENT '用户寄回物流公司',
|
||||
`merchantLogisticsNo` VARCHAR(191) NULL COMMENT '商家换货物流单号',
|
||||
`merchantLogisticsCompany` VARCHAR(191) NULL COMMENT '商家换货物流公司',
|
||||
`applyTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '申请时间',
|
||||
`auditTime` DATETIME(3) NULL COMMENT '审核时间',
|
||||
`userShipTime` DATETIME(3) NULL COMMENT '用户寄回时间',
|
||||
`merchantReceiveTime` DATETIME(3) NULL COMMENT '商家收货时间',
|
||||
`merchantShipTime` DATETIME(3) NULL COMMENT '商家发货时间',
|
||||
`finishTime` DATETIME(3) NULL COMMENT '完成时间',
|
||||
`requestBody` JSON NULL COMMENT '原始业务请求体',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_after_sale_serviceOrderId_key` (`serviceOrderId`),
|
||||
KEY `supplier_after_sale_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_after_sale_supplierId_userId_idx` (`supplierId`, `userId`),
|
||||
KEY `supplier_after_sale_subOrderId_idx` (`subOrderId`),
|
||||
KEY `supplier_after_sale_serviceType_serviceState_idx` (`serviceType`, `serviceState`),
|
||||
KEY `supplier_after_sale_auditStatus_logisticsState_idx` (`auditStatus`, `logisticsState`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商退款售后流程表';
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `supplier_info`
|
||||
ADD COLUMN `supplierApiUrl` VARCHAR(1000) NULL COMMENT '供应商开放接口地址' AFTER `clientSecret`,
|
||||
ADD COLUMN `sroNo` VARCHAR(100) NULL COMMENT 'BAB7服务商编号' AFTER `supplierApiUrl`;
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_user_address` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '地址主键ID',
|
||||
`addressId` VARCHAR(191) NOT NULL COMMENT '地址业务ID',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NOT NULL COMMENT '用户ID',
|
||||
`name` VARCHAR(191) NOT NULL COMMENT '收货人姓名',
|
||||
`tel` VARCHAR(191) NOT NULL COMMENT '收货人手机号',
|
||||
`province` VARCHAR(191) NULL COMMENT '省',
|
||||
`city` VARCHAR(191) NULL COMMENT '市',
|
||||
`county` VARCHAR(191) NULL COMMENT '区县',
|
||||
`addressDetail` TEXT NULL COMMENT '详细地址',
|
||||
`areaCode` VARCHAR(191) NULL COMMENT '地区编码',
|
||||
`postalCode` VARCHAR(191) NULL COMMENT '邮政编码',
|
||||
`isDefault` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否默认地址',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_user_address_addressId_key` (`addressId`),
|
||||
KEY `supplier_user_address_supplierId_userId_idx` (`supplierId`, `userId`),
|
||||
KEY `supplier_user_address_supplierId_userId_isDefault_idx` (`supplierId`, `userId`, `isDefault`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商用户收货地址表';
|
||||
@@ -0,0 +1,79 @@
|
||||
// Supplier order states and operation codes shared by order services and controllers.
|
||||
export const ORDER_STATE = {
|
||||
PENDING_PAY: 0,
|
||||
FINISH_PAY: 1,
|
||||
IN_PROGRESS: 2,
|
||||
CANCELLED: 3,
|
||||
RECEIVED: 4,
|
||||
COMPLETED: 5,
|
||||
EXCHANGE: 6,
|
||||
REFUNDED: 7
|
||||
} as const
|
||||
|
||||
export const ORDER_SPLIT_STATE = {
|
||||
NOT_SPLIT: 0,
|
||||
SPLIT: 1
|
||||
} as const
|
||||
|
||||
export const ORDER_EVENT_TYPE = {
|
||||
CANCEL_MAIN: 1,
|
||||
SPLIT_SUB_ORDER: 2,
|
||||
CANCEL_SUB: 3,
|
||||
RETURN_ORDER: 4,
|
||||
EXCHANGE_ORDER: 5,
|
||||
CONFIRM_RECEIVE: 6,
|
||||
CANCEL_PAID_NOT_SPLIT_MAIN: 7,
|
||||
INVOICE_INFO: 11,
|
||||
SHIP_ORDER: 99
|
||||
} as const
|
||||
|
||||
export const POINTS_LEDGER_BIZ_TYPE = {
|
||||
PAY_CONSUME: 1,
|
||||
CANCEL_REFUND: 2,
|
||||
REFUND: 3,
|
||||
MANUAL_ADJUST: 4
|
||||
} as const
|
||||
|
||||
export const POINTS_LEDGER_DIRECTION = {
|
||||
INCREASE: 1,
|
||||
DECREASE: 2
|
||||
} as const
|
||||
|
||||
export const ORDER_PUSH_STATUS = {
|
||||
PENDING: 0,
|
||||
SUCCESS: 1,
|
||||
FAILED: 2
|
||||
} as const
|
||||
|
||||
export const AFTER_SALE_SERVICE_STATE = {
|
||||
PENDING_AUDIT: 0,
|
||||
APPROVED: 1,
|
||||
REJECTED: 2,
|
||||
USER_SHIPPED: 3,
|
||||
RECEIVED: 4,
|
||||
FINISHED: 5,
|
||||
CANCELLED: 6
|
||||
} as const
|
||||
|
||||
export const AFTER_SALE_LOGISTICS_STATE = {
|
||||
NONE: 0,
|
||||
WAIT_USER_SHIP: 1,
|
||||
USER_SHIPPED: 2,
|
||||
MERCHANT_RECEIVED: 3,
|
||||
MERCHANT_SHIPPED: 4,
|
||||
USER_RECEIVED: 5
|
||||
} as const
|
||||
|
||||
export const ORDER_OPERATE_TYPE = {
|
||||
CREATE_PENDING: 1,
|
||||
PAY_SUCCESS: 2,
|
||||
SPLIT_SUB_ORDER: 3,
|
||||
CANCEL_MAIN: 4,
|
||||
CANCEL_SUB: 5,
|
||||
CANCEL_PAID_NOT_SPLIT_MAIN: 6,
|
||||
RECEIVE: 7,
|
||||
COMPLETE: 8,
|
||||
REFUND: 9,
|
||||
EXCHANGE: 10,
|
||||
INVOICE_INFO: 12
|
||||
} as const
|
||||