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 } from '@/api/http'
|
||||||
import { setupHttp as setupHttp1} from '@/api-collect-code/http'
|
import { setupHttp as setupHttp1} from '@/api-collect-code/http'
|
||||||
|
import { setupHttp as setupHttp2} from '@/api-public/http'
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
setupHttp()
|
setupHttp()
|
||||||
setupHttp1()
|
setupHttp1()
|
||||||
|
setupHttp2()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -248,6 +248,7 @@ export const liveStore = createGlobalState(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return{
|
return{
|
||||||
|
decryptUtils,
|
||||||
wsClient,
|
wsClient,
|
||||||
fullLive,
|
fullLive,
|
||||||
isMinWindow,
|
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