feat(api-public): 新增公共直播室相关功能
- 添加 HTTP 请求工具和 API 接口定义 - 实现公共直播室页面组件和业务逻辑 - 集成阿里云播放器 - 添加指纹识别功能 - 优化错误处理和国际化支持
This commit is contained in:
parent
083ef52e5d
commit
4041b45cca
125
app/api-public/http.js
Normal file
125
app/api-public/http.js
Normal file
@ -0,0 +1,125 @@
|
||||
import {useRuntimeConfig} from '#app'
|
||||
import {ofetch} from 'ofetch'
|
||||
import {message} from '@/components/x-message/useMessage.js'
|
||||
import { getFingerprint } from '@/utils/fingerprint'
|
||||
let httpStatusErrorHandler
|
||||
let http
|
||||
|
||||
// HTTP 状态码映射 - 使用i18n国际化
|
||||
export function setupHttp() {
|
||||
if (http) return http
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const baseURL = config.public.NUXT_PUBLIC_API_BASE
|
||||
const router = useRouter()
|
||||
const i18n = useNuxtApp().$i18n
|
||||
|
||||
// 国际化的HTTP状态码映射
|
||||
const HTTP_STATUS_MAP = {
|
||||
400: i18n.t('http.error.badRequest'),
|
||||
401: i18n.t('http.error.unauthorized'),
|
||||
403: i18n.t('http.error.forbidden'),
|
||||
404: i18n.t('http.error.notFound'),
|
||||
500: i18n.t('http.error.serverError'),
|
||||
502: i18n.t('http.error.badGateway'),
|
||||
503: i18n.t('http.error.serviceUnavailable'),
|
||||
504: i18n.t('http.error.gatewayTimeout')
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
baseURL,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 15000, // 15秒超时
|
||||
retry: 3,
|
||||
retryDelay: 1000,
|
||||
}
|
||||
|
||||
http = ofetch.create({
|
||||
...defaultOptions,
|
||||
|
||||
// 请求拦截
|
||||
async onRequest({ options, request }) {
|
||||
const fingerprint = await getFingerprint()
|
||||
console.log('fingerprint',fingerprint)
|
||||
// 添加 token
|
||||
options.headers = {
|
||||
'Authorization': '12312',
|
||||
...options.headers,
|
||||
'fingerprint':fingerprint,
|
||||
'accept-language': i18n.locale.value
|
||||
}
|
||||
|
||||
// GET 请求添加时间戳防止缓存
|
||||
if (request.toLowerCase().includes('get')) {
|
||||
options.params = {
|
||||
...options.params,
|
||||
_t: Date.now()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 响应拦截
|
||||
async onResponse({ response }) {
|
||||
const data = response._data
|
||||
|
||||
// 处理业务错误
|
||||
if (data.status === 1) {
|
||||
message.error(data.msg || i18n.t('http.error.operationFailed'))
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
|
||||
// 响应错误处理
|
||||
async onResponseError({ response, request }) {
|
||||
// 网络错误
|
||||
if (!response) {
|
||||
message.error(i18n.t('http.error.networkError'))
|
||||
return Promise.reject(new Error(i18n.t('http.error.networkError')))
|
||||
}
|
||||
const status = response.status
|
||||
const data = response._data
|
||||
|
||||
// 处理 HTTP 状态错误
|
||||
const errorMessage = data.msg || HTTP_STATUS_MAP[status] || i18n.t('http.error.requestFailed')
|
||||
|
||||
if (Array.isArray(data.msg)) {
|
||||
data.msg.forEach(item => {
|
||||
httpStatusErrorHandler?.(item, status)
|
||||
})
|
||||
} else {
|
||||
httpStatusErrorHandler?.(errorMessage, status)
|
||||
}
|
||||
|
||||
message.error(errorMessage)
|
||||
return Promise.reject(data)
|
||||
},
|
||||
})
|
||||
|
||||
return http
|
||||
}
|
||||
|
||||
export function createAbortController() {
|
||||
return new AbortController()
|
||||
}
|
||||
|
||||
export function injectHttpStatusErrorHandler(handler) {
|
||||
httpStatusErrorHandler = handler
|
||||
}
|
||||
|
||||
export function getHttp() {
|
||||
if (!http) {
|
||||
throw new Error(useNuxtApp().$i18n.t('http.error.httpNotInitialized'))
|
||||
}
|
||||
return http
|
||||
}
|
||||
|
||||
// 导出请求工具函数
|
||||
export async function request({url,...options}) {
|
||||
const http = getHttp()
|
||||
try {
|
||||
return await http(url, {...options,body:options.data})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
25
app/api-public/public/index.js
Normal file
25
app/api-public/public/index.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { request } from "../http";
|
||||
|
||||
export async function defaultDetail(data) {
|
||||
|
||||
return await request( {
|
||||
url:'/api/v1/m/auction/out/default/detail',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
export async function getLink(data) {
|
||||
|
||||
return await request( {
|
||||
url:'/api/v1/m/auction/out/log/sendlog/aljdfoqueoirhkjsadhfiu',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
export async function outBuyList(data) {
|
||||
return await request( {
|
||||
url:'/api/v1/m/auction/out/buy/list',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
28
app/api/public/index.js
Normal file
28
app/api/public/index.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { request } from "../http";
|
||||
|
||||
export async function defaultDetail(data) {
|
||||
|
||||
return await request( {
|
||||
url:'/api/v1/m/auction/out/default/detail',
|
||||
headers:{
|
||||
'fingerprint':'12312'
|
||||
},
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
export async function getLink(data) {
|
||||
|
||||
return await request( {
|
||||
url:'/api/v1/m/auction/out/log/sendlog/aljdfoqueoirhkjsadhfiu',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
export async function outBuyList(data) {
|
||||
return await request( {
|
||||
url:'/api/v1/m/auction/out/buy/list',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
76
app/pages/publicLiveRoom/components/broadcast/index.vue
Normal file
76
app/pages/publicLiveRoom/components/broadcast/index.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import {publicStore} from "@/stores/public/index.js";
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {outBuyList} from "@/api-public/public/index.js";
|
||||
|
||||
const {auctionData} = publicStore()
|
||||
function formatThousands(num) {
|
||||
|
||||
return Number(num).toLocaleString();
|
||||
}
|
||||
const headList=[
|
||||
{
|
||||
label:useI18n().t('live_room.head'),
|
||||
color:'#D03050',
|
||||
value:'head'
|
||||
},
|
||||
{
|
||||
label:useI18n().t('live_room.out'),
|
||||
color:'#939393',
|
||||
value:'out'
|
||||
},
|
||||
{
|
||||
label:useI18n().t('live_room.success'),
|
||||
color:'#34B633',
|
||||
value:'success'
|
||||
}
|
||||
]
|
||||
const buyList=ref([])
|
||||
const headItem=(statusCode)=>{
|
||||
return headList.find(x=>x.value===statusCode)
|
||||
}
|
||||
onMounted(async()=>{
|
||||
const res=await outBuyList({uuid:auctionData.value.uuid})
|
||||
buyList.value=res.data.buys
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="list-container"
|
||||
class="w-344px h-86px overflow-y-auto bg-#fff rounded-4px text-14px text-#939393 pt-7px pb-7px px-11px flex flex-col justify-between"
|
||||
>
|
||||
<transition-group name="list" tag="div">
|
||||
|
||||
<template v-if="buyList?.length>0">
|
||||
<div v-for="(item, index) in buyList" :key="index" class="flex flex-shrink-0">
|
||||
<!-- 将每列宽度改为相等(约86px),添加文本溢出处理 -->
|
||||
<div class="text-start shrink-0 w-1/4 truncate" :style="`color: ${headItem(item.statusCode).color}`">
|
||||
{{ headItem(item.statusCode).label }}
|
||||
</div>
|
||||
<div class="text-start shrink-0 w-1/4 truncate">
|
||||
{{ item.auctionType==='local'? $t('live_room.spot'):$t('live_room.network') }}
|
||||
</div>
|
||||
<div class="text-start shrink-0 w-1/4 truncate">
|
||||
{{ item.createdAt }}
|
||||
</div>
|
||||
<div class="text-start shrink-0 w-1/4 truncate">
|
||||
{{item.baseCurrency}}{{ formatThousands(item.baseMoney) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-enter-active, .list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.list-enter-from, .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
</style>
|
123
app/pages/publicLiveRoom/index.client.vue
Normal file
123
app/pages/publicLiveRoom/index.client.vue
Normal file
@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { defaultDetail,getLink } from '@/api-public/public/index.js'
|
||||
import { liveStore } from '@/stores/live/index.js'
|
||||
import AliyunPlayer from 'aliyun-aliplayer'
|
||||
import { publicStore } from '@/stores/public/index.js'
|
||||
import 'aliyun-aliplayer/build/skins/default/aliplayer-min.css'
|
||||
import broadcast from './components/broadcast/index.vue'
|
||||
const {decryptUtils} = liveStore()
|
||||
const {auctionData} = publicStore()
|
||||
definePageMeta({
|
||||
layout: 'publicLiveRoom'
|
||||
})
|
||||
const {t}= useI18n()
|
||||
const pullLink = ref('')
|
||||
const player = ref(null)
|
||||
const loading1=ref(false)
|
||||
const handlePlayerError = (error) => {
|
||||
showConfirmDialog({
|
||||
message: t('live_room.error_mess'),
|
||||
showCancelButton: true
|
||||
}).then(() => {
|
||||
initializePlayer()
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
const initializePlayer = async () => {
|
||||
try {
|
||||
if (player.value) {
|
||||
player.value.dispose()
|
||||
}
|
||||
|
||||
// 判断是否是微信浏览器
|
||||
const isWechat = /MicroMessenger/i.test(navigator.userAgent)
|
||||
|
||||
const playerConfig = {
|
||||
id: 'J_prismPlayer1',
|
||||
source: pullLink.value,
|
||||
isLive: true,
|
||||
preload: true,
|
||||
autoplay: true, // 改为 true
|
||||
muted: true, // 默认静音
|
||||
diagnosisButtonVisible:false,
|
||||
// vodRetry:10,
|
||||
// liveRetry:10,
|
||||
autoplayPolicy: {
|
||||
fallbackToMute: true
|
||||
},
|
||||
width: '100%', //容器的大小
|
||||
height: '100%', //容器的大小
|
||||
skinLayout: false,
|
||||
controlBarVisibility: 'never',
|
||||
license: {
|
||||
domain: "szjixun.cn",
|
||||
key: "OProxmWaOZ2XVHXLtf4030126521c43429403194970aa8af9"
|
||||
}
|
||||
}
|
||||
|
||||
player.value = new AliyunPlayer(playerConfig, (playerInstance) => {
|
||||
// 在微信环境下,需要用户手动触发播放
|
||||
if (isWechat) {
|
||||
const startPlay = () => {
|
||||
playerInstance?.play()
|
||||
document.removeEventListener('WeixinJSBridgeReady', startPlay)
|
||||
document.removeEventListener('touchstart', startPlay)
|
||||
}
|
||||
document.addEventListener('WeixinJSBridgeReady', startPlay)
|
||||
document.addEventListener('touchstart', startPlay)
|
||||
}
|
||||
loading1.value = true
|
||||
playerInstance?.play()
|
||||
})
|
||||
player.value.on('playing', () => {
|
||||
loading1.value = false
|
||||
|
||||
})
|
||||
player.value.on('loading', () => {
|
||||
})
|
||||
player.value.on('error', handlePlayerError)
|
||||
} catch (error) {
|
||||
console.log('error',error)
|
||||
showConfirmDialog({
|
||||
message: t('live_room.error_mess'),
|
||||
showCancelButton: true
|
||||
}).then(() => {
|
||||
initializePlayer()
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
const res = await defaultDetail({})
|
||||
if(res.status === 0){
|
||||
auctionData.value = res.data
|
||||
}
|
||||
const linkRes = await getLink({uuid:auctionData.value.uuid})
|
||||
pullLink.value =decryptUtils.decryptData(linkRes.data.code)
|
||||
initializePlayer()
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grow-1 relative">
|
||||
<van-nav-bar :title="auctionData.title" />
|
||||
<div id="J_prismPlayer1" class="w-100vw" style="height: calc(100vh - var(--van-nav-bar-height));"></div>
|
||||
<div v-if="loading1" class="absolute left-1/2 transform translate-x--1/2 top-1/2 translate-y--1/2">
|
||||
<van-loading type="spinner" >直播加载中...</van-loading>
|
||||
</div>
|
||||
<div class="absolute left-1/2 transform -translate-x-1/2" style="bottom:calc(var(--safe-area-inset-bottom) + 46px)">
|
||||
<broadcast></broadcast>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@ -1,6 +1,9 @@
|
||||
import { setupHttp } from '@/api/http'
|
||||
import { setupHttp as setupHttp1} from '@/api-collect-code/http'
|
||||
import { setupHttp as setupHttp2} from '@/api-public/http'
|
||||
export default defineNuxtPlugin(() => {
|
||||
setupHttp()
|
||||
setupHttp1()
|
||||
setupHttp2()
|
||||
})
|
||||
|
||||
|
@ -248,6 +248,7 @@ export const liveStore = createGlobalState(() => {
|
||||
}
|
||||
}
|
||||
return{
|
||||
decryptUtils,
|
||||
wsClient,
|
||||
fullLive,
|
||||
isMinWindow,
|
||||
|
8
app/stores/public/index.js
Normal file
8
app/stores/public/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import {createGlobalState, useLocalStorage} from '@vueuse/core'
|
||||
export const publicStore = createGlobalState(() => {
|
||||
const auctionData=useLocalStorage('auctionData',{})
|
||||
return {
|
||||
auctionData
|
||||
}
|
||||
})
|
||||
|
17
app/utils/fingerprint.js
Normal file
17
app/utils/fingerprint.js
Normal file
@ -0,0 +1,17 @@
|
||||
import FingerprintJS from '@fingerprintjs/fingerprintjs'
|
||||
|
||||
export async function getFingerprint() {
|
||||
try {
|
||||
// 初始化 FingerprintJS
|
||||
const fp = await FingerprintJS.load()
|
||||
|
||||
// 获取访问者的指纹
|
||||
const result = await fp.get()
|
||||
|
||||
// 返回指纹哈希值
|
||||
return result.visitorId
|
||||
} catch (error) {
|
||||
console.error('获取浏览器指纹失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user