feat: 基本实现拆单收货

This commit is contained in:
2026-07-29 15:53:23 +08:00
parent c443109869
commit 1b45f9dac6
92 changed files with 3223 additions and 22968 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/.netlify/functions'
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/api'
const normalizeBase = base => base.replace(/\/$/, '')
+2
View File
@@ -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
+2
View File
@@ -13,6 +13,7 @@ import JoyfulSelectionPaymentSuccess from '@/view/joyful-selection/order/payment
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 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'
@@ -41,6 +42,7 @@ export default new Router({
{ path: 'address', component: JoyfulSelectionAddressList },
{ path: 'address/edit', component: JoyfulSelectionAddressEdit },
{ path: 'orders', component: JoyfulSelectionOrders },
{ path: 'order-detail', component: JoyfulSelectionOrderDetail },
{ 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>
+12
View File
@@ -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>
+3 -26
View File
@@ -23,15 +23,12 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
supplierIdentification: '',
supplierId: '',
supplierName: '',
token: '',
refreshToken: '',
tokenExpiresAt: 0,
pointsBalance: 0,
loaded: false,
loadingToken: false
}),
getters: {
hasToken: state => Boolean(state.token && state.tokenExpiresAt > Date.now())
hasToken: state => Boolean(state.supplierId && state.userId)
},
actions: {
loadUser() {
@@ -43,9 +40,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
this.supplierIdentification = supplierIdentification
this.supplierId = supplierChanged ? '' : data.supplierId || ''
this.supplierName = supplierChanged ? '' : data.supplierName || ''
this.token = supplierChanged ? '' : data.token || ''
this.refreshToken = supplierChanged ? '' : data.refreshToken || ''
this.tokenExpiresAt = supplierChanged ? 0 : Number(data.tokenExpiresAt || 0)
this.pointsBalance = supplierChanged ? 0 : Number(data.pointsBalance || 0)
this.loaded = true
},
@@ -55,9 +49,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
supplierIdentification: this.supplierIdentification,
supplierId: this.supplierId,
supplierName: this.supplierName,
token: this.token,
refreshToken: this.refreshToken,
tokenExpiresAt: this.tokenExpiresAt,
pointsBalance: this.pointsBalance
})
this.loaded = true
@@ -71,9 +62,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
if (nextUserId && nextUserId !== this.userId) {
this.userId = nextUserId
this.token = ''
this.refreshToken = ''
this.tokenExpiresAt = 0
this.pointsBalance = 0
}
if (!this.supplierIdentification) {
@@ -94,12 +82,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
supplierIdentification: this.supplierIdentification
})
if (data?.supplierId && data.supplierId !== this.supplierId) {
this.token = ''
this.refreshToken = ''
this.tokenExpiresAt = 0
}
this.supplierId = data?.supplierId || ''
this.supplierName = data?.supplierName || ''
this.persistUser()
@@ -128,10 +110,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
this.supplierId = data?.supplierId || this.supplierId
this.supplierName = data?.supplierName || this.supplierName
this.token = data?.token || data?.accessToken || ''
this.refreshToken = data?.refreshToken || ''
const expiresIn = Number(data?.expiresIn || 0)
this.tokenExpiresAt = this.token && expiresIn > 0 ? Date.now() + expiresIn * 1000 : 0
this.persistUser()
})()
@@ -148,14 +126,13 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
}
await this.fetchBridgeToken()
if (!this.userId || !this.supplierId || !this.token) {
if (!this.userId || !this.supplierId) {
return this.pointsBalance
}
const data = await postFunction('supplier-points', {
userId: this.userId,
supplierId: this.supplierId,
token: this.token
supplierId: this.supplierId
})
this.pointsBalance = Number(data?.userPoints || 0)
+15 -3
View File
@@ -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>
+15 -3
View File
@@ -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>
+15 -3
View File
@@ -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>
+15 -3
View File
@@ -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>
+15 -2
View File
@@ -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,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"
@@ -14,17 +16,23 @@
@delete="onDelete"
@change-detail="onChangeDetail"
/>
</main>
</main>
</div>
</template>
<script>
import { Toast } from 'vant'
import { areaList } from '@vant/area-data'
import { createAddressId, readAddresses, saveAddresses, 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: {}
@@ -43,6 +51,9 @@ export default {
if (address) {
this.addressInfo = { ...address }
}
this.$nextTick(() => {
this.loading = false
})
},
methods: {
onSave(content) {
@@ -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,15 +13,21 @@
@edit="onEdit"
@select="onSelect"
/>
</main>
</main>
</div>
</template>
<script>
import { formatAddress, readAddresses, 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: []
@@ -38,6 +46,9 @@ export default {
created() {
this.addresses = readAddresses()
this.chosenAddressId = readChosenAddressId()
this.$nextTick(() => {
this.loading = false
})
},
methods: {
onSelect(item) {
@@ -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,17 +1,30 @@
<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) {
@@ -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">
@@ -44,16 +46,22 @@
</article>
</div>
</section>
</main>
</main>
</div>
</template>
<script>
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' },
@@ -83,12 +91,15 @@ export default {
},
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) {
@@ -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>
@@ -14,9 +15,16 @@
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() {
@@ -42,10 +50,13 @@ export default {
...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
})
}
}
@@ -1,5 +1,7 @@
<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' } })">
@@ -50,7 +52,8 @@
</section>
<van-submit-bar :price="goodsAmount" button-text="提交订单" @submit="onSubmit" />
</main>
</main>
</div>
</template>
<script>
@@ -60,12 +63,17 @@ import { useCartStore } from '@/store/cart'
import { useUserStore } from '@/store/user'
import { readAddresses, readChosenAddressId } from '../../address/address-data'
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,
@@ -74,7 +82,7 @@ export default {
}),
computed: {
...mapState(useCartStore, ['items']),
...mapState(useUserStore, ['supplierId', 'token', 'userId']),
...mapState(useUserStore, ['supplierId', 'userId']),
visibleGoods() {
return this.showAllGoods ? this.goods : this.goods.slice(0, 3)
},
@@ -89,12 +97,15 @@ export default {
this.loadCart()
this.goods = this.getCheckoutGoods()
this.loadAddress()
this.$nextTick(() => {
this.loading = false
})
},
activated() {
this.loadAddress()
},
methods: {
...mapActions(useCartStore, ['loadCart']),
...mapActions(useCartStore, ['loadCart', 'removeItems']),
...mapActions(useUserStore, ['fetchBridgeToken']),
getCheckoutGoods() {
const storedItems = this.readCheckoutItems()
@@ -136,7 +147,7 @@ export default {
try {
await this.fetchBridgeToken()
if (!this.supplierId || !this.token || !this.userId) {
if (!this.supplierId || !this.userId) {
throw new Error('供应商授权信息不完整,请重新进入商城')
}
@@ -144,7 +155,6 @@ export default {
const data = await postFunction('order-create', {
supplierId: this.supplierId,
userId: this.userId,
token: this.token,
orderNo,
amount: this.goodsAmount,
goods: this.goods
@@ -158,6 +168,8 @@ export default {
receiverTel: this.maskTel(this.chosenAddress.tel || ''),
goods: this.goods
})
this.clearSubmittedCartItems()
this.$router.push('/joyful-selection/payment')
} catch (error) {
console.warn('提交待支付订单失败', error)
@@ -165,6 +177,19 @@ export default {
} 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)
}
}
}
@@ -1,5 +1,7 @@
<template>
<main class="success-page">
<div>
<joyful-selection-payment-success-skeleton v-if="loading" />
<main v-else class="success-page">
<van-nav-bar title="支付成功" left-arrow @click-left="$router.back()" />
<section class="success-card">
@@ -20,15 +22,21 @@
<van-button round type="danger" @click="$router.push('/joyful-selection/home')">继续购物</van-button>
</footer>
</section>
</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: ''
}),
@@ -49,6 +57,9 @@ export default {
created() {
this.paymentInfo = readPaymentInfo()
this.paidAt = this.formatTime(new Date())
this.$nextTick(() => {
this.loading = false
})
},
methods: {
formatTime(date) {
@@ -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">
@@ -61,7 +63,8 @@
</footer>
</article>
</van-popup>
</main>
</main>
</div>
</template>
<script>
@@ -69,17 +72,22 @@ 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',
showConfirm: false,
submitting: false,
paymentInfo: {}
}),
computed: {
...mapState(useUserStore, ['pointsBalance', 'supplierId', 'token']),
...mapState(useUserStore, ['pointsBalance', 'supplierId']),
amountText() {
return formatYuan(this.paymentInfo.amount)
},
@@ -110,9 +118,12 @@ export default {
},
created() {
this.paymentInfo = readPaymentInfo()
this.loading = true
this.fetchUserPoints().catch(error => {
console.warn('查询用户积分失败', error)
this.$toast(error.message || '查询用户积分失败')
}).finally(() => {
this.loading = false
})
},
methods: {
@@ -129,13 +140,12 @@ export default {
try {
await this.fetchBridgeToken()
if (!this.supplierId || !this.token || !this.paymentInfo.orderNo) {
if (!this.supplierId || !this.paymentInfo.orderNo) {
throw new Error('订单支付信息不完整,请重新提交订单')
}
await postFunction('order-pay', {
supplierId: this.supplierId,
token: this.token,
orderId: this.paymentInfo.orderNo,
amount: this.paymentInfo.amount,
totalPrice: Number(this.amountText),
@@ -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>
@@ -77,7 +79,8 @@
@checkout="goCheckout"
/>
</van-popup>
</main>
</main>
</div>
</template>
<script>
@@ -86,6 +89,7 @@ import { getFunction } from '@/api/supplied-service'
import { useCartStore } from '@/store/cart'
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: '',
@@ -102,9 +106,11 @@ export default {
name: 'JoyfulSelectionProductDetail',
components: {
JoyfulSkuPopup,
JoyfulCartPanel
JoyfulCartPanel,
JoyfulSelectionProductDetailSkeleton
},
data: () => ({
loading: true,
product: emptyProduct,
showCartPopup: false,
count: 1,
@@ -131,12 +137,15 @@ export default {
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() {
@@ -110,20 +110,20 @@ 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,
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">
@@ -53,12 +55,14 @@
</section>
<joyful-sku-popup ref="skuPopup" />
</main>
</main>
</div>
</template>
<script>
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: '推荐' }
@@ -67,9 +71,11 @@ const defaultCategories = [
export default {
name: 'JoyfulSelectionProductList',
components: {
JoyfulSkuPopup
JoyfulSkuPopup,
JoyfulSelectionProductListSkeleton
},
data: () => ({
loading: true,
selectedCategoryId: 'recommend',
keyword: '',
categories: defaultCategories,
@@ -91,6 +97,7 @@ export default {
},
methods: {
async loadProducts() {
this.loading = true
try {
const data = await getFunction('product-list')
this.categories = data?.categories?.length ? data.categories : defaultCategories
@@ -98,6 +105,8 @@ export default {
} catch (error) {
console.warn('查询商品列表失败', error)
this.$toast(error.message || '查询商品列表失败')
} finally {
this.loading = false
}
},
goProductDetail(item) {
@@ -1,5 +1,7 @@
<template>
<main class="profile-page">
<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" />
@@ -69,16 +71,22 @@
<p>敬请期待</p>
</div>
</section>
</main>
</main>
</div>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import { useUserStore } from '@/store/user'
import JoyfulSelectionProfileSkeleton from '@/skeleton-view/joyful-selection/profile/index.vue'
export default {
name: 'JoyfulSelectionProfile',
components: {
JoyfulSelectionProfileSkeleton
},
data: () => ({
loading: true,
orderItems: [
{ icon: 'balance-pay', text: '待付款' },
{ icon: 'logistics', text: '待发货' },
@@ -96,9 +104,12 @@ export default {
}
},
created() {
this.loading = true
this.fetchUserPoints().catch(error => {
console.warn('查询用户积分失败', error)
this.$toast(error.message || '查询用户积分失败')
}).finally(() => {
this.loading = false
})
},
methods: {
@@ -0,0 +1,240 @@
<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">
<h2>{{ currentSubOrder.status }}</h2>
<p>主单号:{{ currentOrder.mainOrderId || '-' }}</p>
<p>子单号:{{ currentSubOrder.orderId || '-' }}</p>
</section>
<section class="goods-card">
<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>SKU:{{ item.sku || '-' }}</p>
<b>¥{{ item.price }}</b>
</div>
<span>x{{ item.number || 1 }}</span>
</article>
</section>
<section class="info-card">
<p><span>订单编号</span><b>{{ currentSubOrder.orderNumberId || '-' }}</b></p>
<p><span>供应商</span><b>{{ currentSubOrder.supplierId || '-' }}</b></p>
<p><span>商品数量</span><b>{{ currentSubOrder.count || 0 }}</b></p>
<p><span>订单金额</span><b class="price">¥{{ currentSubOrder.amount }}</b></p>
</section>
</template>
</main>
</div>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import { Toast } from 'vant'
import { postFunction } from '@/api/supplied-service'
import { useUserStore } from '@/store/user'
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' }
}
export default {
name: 'JoyfulSelectionOrderDetail',
components: {
JoyfulSelectionOrderDetailSkeleton
},
data: () => ({
loading: true,
currentOrder: null,
currentSubOrder: null
}),
computed: {
...mapState(useUserStore, ['supplierId', 'userId'])
},
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
}
},
async loadOrderDetail() {
this.loading = true
try {
await this.fetchBridgeToken()
if (!this.supplierId || !this.userId) {
Toast('用户信息不完整,请重新进入商城')
return
}
const data = await postFunction('order-list', {
supplierId: this.supplierId,
userId: this.userId
})
const mainOrderId = String(this.$route.query.mainOrderId || '')
const orderId = String(this.$route.query.orderId || '')
const order = (data?.orders || []).find(item => item.mainOrderId === mainOrderId)
const subOrder = order?.subOrders?.find(item => item.orderId === orderId)
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 0 16px;
background: #f7f7f7;
}
.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,
.info-card {
margin: 10px 12px 0;
border-radius: 8px;
background: #fff;
}
.status-card {
padding: 16px 14px;
}
.status-card h2 {
margin: 0 0 10px;
color: #ff5b20;
font-size: 18px;
}
.status-card p {
margin: 4px 0 0;
color: #666;
font-size: 12px;
}
.goods-card {
padding: 2px 12px;
}
.goods-item {
display: flex;
align-items: center;
min-height: 86px;
padding: 12px 0;
}
.goods-item + .goods-item {
border-top: 1px solid #f4f4f4;
}
.goods-thumb {
width: 64px;
height: 64px;
flex: none;
overflow: hidden;
border-radius: 4px;
background: #f7f7f7;
}
.goods-info {
min-width: 0;
flex: 1;
padding: 0 10px;
}
.goods-info h3 {
display: -webkit-box;
overflow: hidden;
margin: 0 0 6px;
color: #222;
font-size: 13px;
font-weight: 700;
line-height: 18px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.goods-info p {
overflow: hidden;
margin: 0 0 6px;
color: #999;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.goods-info b,
.price {
color: #ff5b20;
}
.goods-item > span {
color: #666;
font-size: 12px;
}
.info-card {
padding: 12px 14px;
}
.info-card p {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
color: #777;
font-size: 13px;
line-height: 30px;
}
.info-card b {
max-width: 66%;
overflow: hidden;
color: #333;
font-weight: 500;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -1,74 +1,184 @@
<template>
<main class="orders-page">
<van-nav-bar title="我的订单" left-arrow @click-left="$router.back()" />
<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" :title="tab" />
</van-tabs>
<div>
<joyful-selection-orders-skeleton v-if="loading" />
<main v-else class="orders-page">
<van-nav-bar title="我的订单" left-arrow @click-left="$router.back()" />
<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" />
</van-tabs>
<section class="order-list">
<van-loading v-if="loading" class="order-loading" color="#ff5b20" size="24px">加载中...</van-loading>
<van-empty v-else-if="visibleOrders.length === 0" description="暂无订单" />
<section class="order-list">
<van-empty v-if="visibleOrders.length === 0" description="暂无订单" />
<template v-else>
<article v-for="order in visibleOrders" :key="order.id" class="order-card">
<header>
<div class="order-nos">
<template v-else>
<article v-for="order in visibleOrders" :key="order.viewId" class="order-card">
<header class="main-order-header">
<span>主单号:{{ order.mainOrderId || '-' }}</span>
<span>子单号:{{ order.orderId || '-' }}</span>
<van-button
v-if="order.canCancel"
class="cancel-main-btn"
size="mini"
round
plain
:loading="isOperating('cancel-main', order.mainOrderId)"
@click.stop="cancelMainOrder(order)"
>
取消订单
</van-button>
</header>
<div class="sub-order-list">
<section
v-for="subOrder in order.subOrders"
:key="subOrder.id"
class="sub-order-row"
@click="goOrderDetail(order, subOrder)"
>
<div class="sub-order-title">
<span>子单号:{{ subOrder.orderId || '-' }}</span>
<b :class="subOrder.statusClass">{{ subOrder.status }}</b>
</div>
<div class="order-body">
<div class="thumb-row">
<van-image
v-for="item in previewProducts(subOrder.products)"
:key="item.id"
class="order-thumb"
:src="item.image"
fit="cover"
/>
</div>
<div class="order-summary">
<p>共{{ subOrder.count }}件商品</p>
<p>合计:<b>¥{{ subOrder.amount }}</b></p>
</div>
</div>
<footer v-if="subActions(subOrder).length">
<van-button
v-for="action in subActions(subOrder)"
:key="action.orderType"
size="small"
round
plain
:class="action.buttonClass"
:loading="isOperating(action.orderType, subOrder.orderId)"
@click.stop="submitSubOrderEvent(order, subOrder, action)"
>
{{ action.text }}
</van-button>
</footer>
</section>
</div>
<b :class="order.statusClass">{{ order.status }}</b>
</header>
<div class="order-body">
<div class="thumb-row">
<van-image
v-for="item in previewProducts(order.products)"
:key="item.id"
class="order-thumb"
:src="item.image"
fit="cover"
/>
</div>
<div class="order-summary">
<p>共{{ order.count }}件商品</p>
<p>合计:<b>¥{{ order.amount }}</b></p>
</div>
</div>
<footer>
<van-button size="small" round plain :class="order.buttonClass">{{ order.actionText }}</van-button>
</footer>
</article>
</template>
</section>
</main>
</article>
</template>
</section>
</main>
</div>
</template>
<script>
import { mapActions, mapState } from 'pinia'
import { Toast } from 'vant'
import { Dialog, Toast } from 'vant'
import { postFunction } from '@/api/supplied-service'
import { useUserStore } from '@/store/user'
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_SPLIT_STATE = {
NOT_SPLIT: 0,
SPLIT: 1
}
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' }
}
export default {
name: 'JoyfulSelectionOrders',
components: {
JoyfulSelectionOrdersSkeleton
},
data: () => ({
active: 0,
tabs: ['全部', '待付款', '待发货', '待收货', '已完成', '售后'],
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 },
{ label: '售后', type: 'after-sale' }
],
orders: [],
loading: false
loading: false,
operationKey: ''
}),
computed: {
...mapState(useUserStore, ['supplierId', 'userId']),
activeTab() {
return this.tabs[this.active] || this.tabs[0]
},
visibleOrders() {
const tab = this.tabs[this.active]
const tab = this.activeTab
return tab === '全部' ? this.orders : this.orders.filter(order => order.status === tab)
if (tab.type === 'all') {
return this.orders.map(order => this.normalizeOrder(order, order.subOrders))
}
if (tab.type === 'pending-main') {
return this.orders
.filter(order => Number(order.orderState) === ORDER_STATE.PENDING_PAY)
.map(order => this.normalizeOrder(order, order.subOrders))
}
if (tab.type === 'sub-state') {
return this.splitSubOrdersByState(tab.state)
}
if (tab.type === 'cancelled') {
return this.cancelledOrders()
}
if (tab.type === 'after-sale') {
return this.orders.flatMap(order => {
const subOrders = this.normalizedSubOrders(order).filter(subOrder => {
return [ORDER_STATE.EXCHANGE, ORDER_STATE.REFUNDED].includes(Number(subOrder.orderState))
})
return subOrders.map(subOrder => this.normalizeOrder(order, [subOrder]))
})
}
return []
}
},
created() {
@@ -76,9 +186,91 @@ export default {
},
methods: {
...mapActions(useUserStore, ['fetchBridgeToken']),
normalizeOrder(order, subOrders = []) {
const sourceSubOrders = subOrders.length ? subOrders : order.subOrders
const normalizedSubOrders = (sourceSubOrders || []).map(subOrder => this.normalizeSubOrder(order, subOrder))
return {
...order,
viewId: `${order.mainOrderId || order.id}-${normalizedSubOrders.map(item => item.orderId).join('-') || 'main'}`,
canCancel: Number(order.orderState) === ORDER_STATE.PENDING_PAY,
subOrders: normalizedSubOrders
}
},
normalizeSubOrder(order, subOrder) {
const view = ORDER_STATE_VIEW[Number(subOrder.orderState)] || ORDER_STATE_VIEW[Number(order.orderState)] || ORDER_STATE_VIEW[ORDER_STATE.IN_PROGRESS]
return {
...subOrder,
status: view.text,
statusClass: view.statusClass,
count: Number(subOrder.count || 0)
}
},
normalizedSubOrders(order) {
return (order.subOrders || []).map(subOrder => this.normalizeSubOrder(order, subOrder))
},
splitSubOrdersByState(state) {
return this.orders.flatMap(order => {
if (Number(order.isSplit) !== ORDER_SPLIT_STATE.SPLIT) {
return []
}
return this.normalizedSubOrders(order)
.filter(subOrder => Number(subOrder.orderState) === state)
.map(subOrder => this.normalizeOrder(order, [subOrder]))
})
},
cancelledOrders() {
return this.orders.flatMap(order => {
const mainCancelled = Number(order.orderState) === ORDER_STATE.CANCELLED
if (mainCancelled && Number(order.isSplit) !== ORDER_SPLIT_STATE.SPLIT) {
return [this.normalizeOrder(order, order.subOrders)]
}
return this.normalizedSubOrders(order)
.filter(subOrder => Number(subOrder.orderState) === ORDER_STATE.CANCELLED)
.map(subOrder => this.normalizeOrder(order, [subOrder]))
})
},
previewProducts(products = []) {
return products.slice(0, 3)
},
actionKey(type, orderId) {
return `${type}-${orderId}`
},
isOperating(type, orderId) {
return this.operationKey === this.actionKey(type, orderId)
},
subActions(subOrder) {
const state = Number(subOrder.orderState)
const actions = []
if (state === ORDER_STATE.IN_PROGRESS) {
actions.push({ orderType: ORDER_EVENT_TYPE.CONFIRM_RECEIVE, text: '确认收货', buttonClass: 'primary-action' })
}
if ([ORDER_STATE.IN_PROGRESS, ORDER_STATE.COMPLETED].includes(state)) {
actions.push({ orderType: ORDER_EVENT_TYPE.RETURN_ORDER, text: '退货', buttonClass: 'dark-action' })
actions.push({ orderType: ORDER_EVENT_TYPE.EXCHANGE_ORDER, text: '换货', buttonClass: 'dark-action' })
}
if (![ORDER_STATE.PENDING_PAY, ORDER_STATE.CANCELLED].includes(state)) {
actions.push({ orderType: ORDER_EVENT_TYPE.INVOICE_INFO, text: '发票信息', buttonClass: 'dark-action' })
}
return actions
},
goOrderDetail(order, subOrder) {
this.$router.push({
path: '/joyful-selection/order-detail',
query: {
mainOrderId: order.mainOrderId,
orderId: subOrder.orderId
}
})
},
async loadOrders() {
this.loading = true
@@ -103,6 +295,84 @@ export default {
} finally {
this.loading = false
}
},
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 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 = ''
}
}
}
}
@@ -146,12 +416,6 @@ export default {
padding: 8px 8px 14px;
}
.order-loading {
display: flex;
justify-content: center;
padding: 42px 0;
}
.order-card {
overflow: hidden;
margin-bottom: 10px;
@@ -159,48 +423,78 @@ export default {
background: #fff;
}
.order-card header {
.main-order-header {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
padding: 12px 12px 8px;
padding: 12px 12px 10px;
border-bottom: 1px solid #f4f4f4;
}
.order-nos {
.main-order-header span {
min-width: 0;
}
.order-nos span {
display: block;
overflow: hidden;
color: #777;
font-size: 11px;
line-height: 18px;
color: #555;
font-size: 12px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-card header b {
.cancel-main-btn {
flex: none;
margin-left: 10px;
color: #ff5b20;
border-color: #ff5b20;
}
.sub-order-row {
padding: 10px 12px 12px;
}
.sub-order-row + .sub-order-row {
border-top: 1px solid #f4f4f4;
}
.sub-order-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.sub-order-title span {
min-width: 0;
overflow: hidden;
color: #777;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.sub-order-title b {
flex: none;
margin-left: 10px;
font-size: 12px;
font-weight: 700;
line-height: 18px;
}
.order-card header .pending {
.sub-order-title .pending {
color: #ff5b20;
}
.order-card header .done {
.sub-order-title .done {
color: #21b36b;
}
.sub-order-title .muted {
color: #999;
}
.order-body {
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 0 12px 10px;
}
.thumb-row {
@@ -238,16 +532,18 @@ export default {
font-size: 12px;
}
.order-card footer {
.sub-order-row footer {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
padding: 0 12px 12px;
gap: 8px;
padding-top: 10px;
}
.order-card footer .van-button {
min-width: 78px;
.sub-order-row footer .van-button {
min-width: 72px;
height: 28px;
padding: 0 14px;
padding: 0 12px;
border-radius: 15px;
font-size: 11px;
}
@@ -1,5 +1,7 @@
<template>
<main class="points-page">
<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>
@@ -24,16 +26,22 @@
/>
<p>没有更多了~</p>
</section>
</main>
</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' }
@@ -48,9 +56,12 @@ export default {
}
},
created() {
this.loading = true
this.fetchUserPoints().catch(error => {
console.warn('查询用户积分失败', error)
this.$toast(error.message || '查询用户积分失败')
}).finally(() => {
this.loading = false
})
},
methods: {