feat(live): 实现直播间竞拍功能并优化相关页面
- 新增 artworkBuy API 实现艺术品购买功能 - 重构 WebSocket连接逻辑,优化消息处理- 更新直播间页面,支持实时竞拍和消息提示 -调整艺术详情和用户中心页面样式 - 优化消息组件样式和展示逻辑
This commit is contained in:
parent
47aa573641
commit
36793c5c5a
@ -38,3 +38,11 @@ export async function userArtwork(data) {
|
|||||||
data
|
data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
export async function artworkBuy(data) {
|
||||||
|
|
||||||
|
return await request( {
|
||||||
|
url:'/api/v1/m/artwork/buy',
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
@ -86,7 +86,7 @@ const typeConfig = {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-if="title.text"
|
v-if="title.text"
|
||||||
class="text-[14px] line-height-none"
|
class="text-[14px] line-height-20px"
|
||||||
:style="{
|
:style="{
|
||||||
color: title.color || 'inherit',
|
color: title.color || 'inherit',
|
||||||
textAlign: title.align || 'left'
|
textAlign: title.align || 'left'
|
||||||
@ -96,7 +96,7 @@ const typeConfig = {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="subTitle.text"
|
v-if="subTitle.text"
|
||||||
class="text-[12px] leading-normal mt-1"
|
class="text-[12px] leading-normal mt-1 line-height-17px"
|
||||||
:style="{
|
:style="{
|
||||||
color: subTitle.color || '#939393',
|
color: subTitle.color || '#939393',
|
||||||
textAlign: subTitle.align || 'left'
|
textAlign: subTitle.align || 'left'
|
||||||
|
@ -33,6 +33,7 @@ const message = {
|
|||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
this.show({ type: 'warning', message: options, duration })
|
this.show({ type: 'warning', message: options, duration })
|
||||||
} else {
|
} else {
|
||||||
|
console.log('options',options)
|
||||||
this.show({
|
this.show({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
...options,
|
...options,
|
||||||
|
@ -18,7 +18,7 @@ initData()
|
|||||||
<div class="relative h-screen-nav flex flex-col">
|
<div class="relative h-screen-nav flex flex-col">
|
||||||
<itemDetail class="grow-1" :detail-info="detail.auctionArtworkInfo"/>
|
<itemDetail class="grow-1" :detail-info="detail.auctionArtworkInfo"/>
|
||||||
<div v-if="[1,3,4].includes(detail.status)" class="h-81px bg-#fff flex justify-center pt-7px">
|
<div v-if="[1,3,4].includes(detail.status)" class="h-81px bg-#fff flex justify-center pt-7px">
|
||||||
<van-button class="w-213px van-btn-h-38px" type="primary">
|
<van-button class="w-213px !h-38px" type="primary">
|
||||||
<span class="text-#fff text-14px">去支付 RMB10,000</span>
|
<span class="text-#fff text-14px">去支付 RMB10,000</span>
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ import paymentInput from '@/pages/liveRoom/components/PaymentInput/index.vue'
|
|||||||
import xButton from '@/components/x-button/index.vue'
|
import xButton from '@/components/x-button/index.vue'
|
||||||
import {goodStore} from "@/stores/goods/index.js";
|
import {goodStore} from "@/stores/goods/index.js";
|
||||||
import {message} from "~/components/x-message/useMessage.js";
|
import {message} from "~/components/x-message/useMessage.js";
|
||||||
|
import {artworkBuy} from "@/api/goods/index.js";
|
||||||
const {auctionDetail,getAuctionDetail} = goodStore();
|
const {auctionDetail,getAuctionDetail} = goodStore();
|
||||||
const player = ref(null)
|
const player = ref(null)
|
||||||
const {quoteStatus, changeStatus, show, playerId, show1,auctionData,getSocketData} = liveStore()
|
const {quoteStatus, changeStatus, show, playerId, show1,auctionData,getSocketData} = liveStore()
|
||||||
@ -83,7 +84,16 @@ watch(()=>{
|
|||||||
fullLive1.value = false
|
fullLive1.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const goBuy=async ()=>{
|
||||||
|
console.log('auctionData',auctionData.value)
|
||||||
|
const res= await artworkBuy({
|
||||||
|
auctionArtworkUuid:auctionData.value?.artwork?.uuid,
|
||||||
|
buyMoney:String(auctionData.value?.nowAuctionPrice?.nextPrice??0)
|
||||||
|
})
|
||||||
|
if (res.status===0){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -110,17 +120,17 @@ watch(()=>{
|
|||||||
<sideButton class="absolute top-196px right-0 z-999"></sideButton>
|
<sideButton class="absolute top-196px right-0 z-999"></sideButton>
|
||||||
<div class="absolute left-1/2 transform -translate-x-1/2 flex flex-col items-center" style="bottom:calc(var(--safe-area-inset-bottom) + 26px)">
|
<div class="absolute left-1/2 transform -translate-x-1/2 flex flex-col items-center" style="bottom:calc(var(--safe-area-inset-bottom) + 26px)">
|
||||||
<div class="text-16px text-#FFB25F font-600">
|
<div class="text-16px text-#FFB25F font-600">
|
||||||
当前价:RMB
|
当前价:{{auctionData?.nowAuctionPrice?.currency}}
|
||||||
<van-rolling-text class="my-rolling-text" :start-num="0" :duration="0.5" :target-num="auctionData?.nowAuctionPrice?.nowPrice??0" direction="up"/>
|
<van-rolling-text class="my-rolling-text" :start-num="0" :duration="0.5" :target-num="auctionData?.nowAuctionPrice?.nowPrice??0" direction="up"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-16px text-#fff font-600">
|
<div class="text-16px text-#fff font-600">
|
||||||
下口价:RMB
|
下口价:{{auctionData?.nowAuctionPrice?.currency}}
|
||||||
<van-rolling-text class="my-rolling-text1" :start-num="0" :duration="0.5" :target-num="auctionData?.nowAuctionPrice?.nextPrice??0" direction="up"/>
|
<van-rolling-text class="my-rolling-text1" :start-num="0" :duration="0.5" :target-num="auctionData?.nowAuctionPrice?.nextPrice??0" direction="up"/>
|
||||||
</div>
|
</div>
|
||||||
<x-button>
|
<x-button @click="goBuy">
|
||||||
<div
|
<div
|
||||||
:class="`w-344px h-[40px] ${quoteStatus ? 'bg-#FFB25F' : 'bg-#D6D6D8'} rounded-4px ${quoteStatus ? 'text-#fff' : 'text-#7D7D7F'} text-14px flex justify-center items-center mt-10px mb-10px`">
|
:class="`w-344px h-[40px] ${quoteStatus ? 'bg-#FFB25F' : 'bg-#D6D6D8'} rounded-4px ${quoteStatus ? 'text-#fff' : 'text-#7D7D7F'} text-14px flex justify-center items-center mt-10px mb-10px`">
|
||||||
{{ quoteStatus ? '确认出价 RMB 3,000' : '点击"开启出价",即刻参与竞拍 ' }}
|
{{ quoteStatus ? `确认出价 ${auctionData?.nowAuctionPrice?.currency} ${auctionData?.nowAuctionPrice?.nextPrice??0}` : '点击"开启出价",即刻参与竞拍 ' }}
|
||||||
</div>
|
</div>
|
||||||
</x-button>
|
</x-button>
|
||||||
<broadcast></broadcast>
|
<broadcast></broadcast>
|
||||||
|
@ -74,7 +74,7 @@ const goDetail=(item)=>{
|
|||||||
<div class="text-#575757 text-14px mb-3px">{{item.userCreatedAt}}</div>
|
<div class="text-#575757 text-14px mb-3px">{{item.userCreatedAt}}</div>
|
||||||
<div class="flex mb-22px" v-for="(item1,index1) of item.list" @click="goDetail(item1)">
|
<div class="flex mb-22px" v-for="(item1,index1) of item.list" @click="goDetail(item1)">
|
||||||
<div class="flex-shrink-0 mr-10px rounded-4px overflow-hidden">
|
<div class="flex-shrink-0 mr-10px rounded-4px overflow-hidden">
|
||||||
<x-image class="w-80px h-80px" :src="item1?.auctionArtworkInfo?.artwork?.hdPic" alt=""/>
|
<x-image class="w-80px h-80px" :src="item1?.auctionArtworkInfo?.artwork?.hdPic" :preview="false" alt=""/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-between grow-1">
|
<div class="flex flex-col justify-between grow-1">
|
||||||
<div class="text-#000 text-16px ellipsis line-height-21px">{{item1?.auctionArtworkInfo?.artworkTitle}}{{item1?.auctionArtworkInfo?.artworkTitle}}{{item1?.auctionArtworkInfo?.artworkTitle}}</div>
|
<div class="text-#000 text-16px ellipsis line-height-21px">{{item1?.auctionArtworkInfo?.artworkTitle}}{{item1?.auctionArtworkInfo?.artworkTitle}}{{item1?.auctionArtworkInfo?.artworkTitle}}</div>
|
||||||
@ -84,7 +84,7 @@ const goDetail=(item)=>{
|
|||||||
<div class="text-#B58047 text-14px line-height-none">成交价:RMB 10,000</div>
|
<div class="text-#B58047 text-14px line-height-none">成交价:RMB 10,000</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="[1,3,4].includes(item1.status)" @click="goPay">
|
<div v-if="[1,3,4].includes(item1.status)" @click="goPay">
|
||||||
<van-button class="w-73px van-btn-h-30px" type="primary"><span class="text-12px">去支付</span></van-button>
|
<van-button class="w-73px !h-30px" type="primary"><span class="text-12px">去支付</span></van-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {authStore} from "~/stores/auth";
|
import {authStore} from "@/stores/auth";
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
|
@ -3,32 +3,37 @@ import {ref} from "vue";
|
|||||||
import {goodStore} from "@/stores/goods/index.js";
|
import {goodStore} from "@/stores/goods/index.js";
|
||||||
import {authStore} from "@/stores/auth/index.js";
|
import {authStore} from "@/stores/auth/index.js";
|
||||||
import {message} from "~/components/x-message/useMessage.js";
|
import {message} from "~/components/x-message/useMessage.js";
|
||||||
|
import { WebSocketClient } from '@/utils/websocket'
|
||||||
|
|
||||||
export const liveStore = createGlobalState(() => {
|
export const liveStore = createGlobalState(() => {
|
||||||
const {auctionDetail,getAuctionDetail} = goodStore();
|
const {auctionDetail,getAuctionDetail} = goodStore();
|
||||||
|
const { token } = authStore()
|
||||||
const quoteStatus = ref(false)
|
const quoteStatus = ref(false)
|
||||||
const show = ref(false)
|
const show = ref(false)
|
||||||
const cleanup = ref(null)
|
const cleanup = ref(null)
|
||||||
const show1=ref(true)
|
const show1=ref(false)
|
||||||
const playerId=ref('J_prismPlayer')
|
const playerId=ref('J_prismPlayer')
|
||||||
const auctionData=ref({})
|
const auctionData=ref({})
|
||||||
|
const socket=ref(null)
|
||||||
|
const config = useRuntimeConfig()
|
||||||
const getSocketData = async () => {
|
const getSocketData = async () => {
|
||||||
if (!auctionDetail.value.uuid) {
|
const wsClient = new WebSocketClient(
|
||||||
await getAuctionDetail()
|
config.public.NUXT_PUBLIC_SOCKET_URL,
|
||||||
}
|
token.value
|
||||||
|
)
|
||||||
const { ws, onMessage } = useWebSocket()
|
const ws = wsClient.connect('/api/v1/m/auction/live', {
|
||||||
// 建立新连接
|
|
||||||
ws.connect('/api/v1/m/auction/live', {
|
|
||||||
auctionUuid: auctionDetail.value.uuid,
|
auctionUuid: auctionDetail.value.uuid,
|
||||||
|
|
||||||
})
|
})
|
||||||
// 保存清理函数
|
|
||||||
cleanup.value = onMessage((data) => {
|
ws.onOpen(() => {
|
||||||
|
console.log('WebSocket connected')
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.onMessage((data) => {
|
||||||
auctionData.value = data.data
|
auctionData.value = data.data
|
||||||
if (auctionData.value.wsType === 'tip') {
|
|
||||||
if (auctionData.value.tip?.tipType === 'falling') {
|
if (data.data?.wsType === 'tip' ) {
|
||||||
|
if (data.data?.tip?.tipType === 'falling'){
|
||||||
message.warning({
|
message.warning({
|
||||||
title: {
|
title: {
|
||||||
text: '即将落槌',
|
text: '即将落槌',
|
||||||
@ -40,9 +45,75 @@ export const liveStore = createGlobalState(() => {
|
|||||||
bottom: '230px'
|
bottom: '230px'
|
||||||
},
|
},
|
||||||
}, 500000)
|
}, 500000)
|
||||||
|
}else if (data.data?.tip?.tipType === 'othersBid'){
|
||||||
|
message.error({
|
||||||
|
title: {
|
||||||
|
text: '已有人出价',
|
||||||
|
color: '#CF3050',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
icon:false,
|
||||||
|
subTitle:{
|
||||||
|
text:'更新后再出价',
|
||||||
|
color: '#939393',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: '151px',
|
||||||
|
bottom: '230px'
|
||||||
|
},
|
||||||
|
}, 500000)
|
||||||
|
}else if (data.data?.tip?.tipType === 'successBid'){
|
||||||
|
message.success({
|
||||||
|
title: {
|
||||||
|
text: '恭喜您,竞拍成功',
|
||||||
|
color: '#18A058',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
icon:false,
|
||||||
|
subTitle:{
|
||||||
|
text:'请缴款',
|
||||||
|
color: '#939393',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: '151px',
|
||||||
|
bottom: '230px'
|
||||||
|
},
|
||||||
|
}, 500000)
|
||||||
|
}else if (data.data?.tip?.tipType === 'artworkOver'){
|
||||||
|
message.success({
|
||||||
|
title: {
|
||||||
|
text: '本拍品已结束',
|
||||||
|
color: '#575757',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
icon:false,
|
||||||
|
subTitle:{
|
||||||
|
text:'请期待下个拍品',
|
||||||
|
color: '#939393',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: '151px',
|
||||||
|
bottom: '230px'
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}else if (data.data?.wsType==='stopArtwor'){
|
||||||
|
quoteStatus.value=false
|
||||||
}
|
}
|
||||||
console.log('auctionData.value', auctionData.value)
|
|
||||||
|
console.log('onmessage', data)
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.onClose(() => {
|
||||||
|
console.log('WebSocket disconnected')
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.onError((error) => {
|
||||||
|
console.error('WebSocket error:', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const changeStatus = () => {
|
const changeStatus = () => {
|
||||||
|
65
app/utils/websocket.ts
Normal file
65
app/utils/websocket.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
export class WebSocketClient {
|
||||||
|
private socket: WebSocket | null = null
|
||||||
|
private baseUrl: string
|
||||||
|
private token: string
|
||||||
|
|
||||||
|
constructor(baseUrl: string, token: string) {
|
||||||
|
this.baseUrl = baseUrl
|
||||||
|
this.token = token
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(path: string, params: Record<string, any> = {}) {
|
||||||
|
// 如果存在旧连接,先关闭
|
||||||
|
this.disconnect()
|
||||||
|
|
||||||
|
// 构建参数对象,自动添加 token
|
||||||
|
const queryParams = {
|
||||||
|
token: this.token,
|
||||||
|
...params
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建查询字符串
|
||||||
|
const queryString = '?' + Object.entries(queryParams)
|
||||||
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||||
|
.join('&')
|
||||||
|
|
||||||
|
// 构建完整的 WebSocket URL
|
||||||
|
const wsUrl = `${this.baseUrl}${path}${queryString}`
|
||||||
|
this.socket = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
return {
|
||||||
|
onOpen: (callback: () => void) => {
|
||||||
|
this.socket!.onopen = callback
|
||||||
|
},
|
||||||
|
onMessage: (callback: (data: any) => void) => {
|
||||||
|
this.socket!.onmessage = (event) => {
|
||||||
|
console.log('event',event)
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
console.log('data',data)
|
||||||
|
callback(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析消息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClose: (callback: () => void) => {
|
||||||
|
this.socket!.onclose = callback
|
||||||
|
},
|
||||||
|
onError: (callback: (error: Event) => void) => {
|
||||||
|
this.socket!.onerror = callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.close()
|
||||||
|
this.socket = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected() {
|
||||||
|
return this.socket?.readyState === WebSocket.OPEN
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user