Compare commits
16 Commits
825ec8affe
...
016623b6a4
Author | SHA1 | Date | |
---|---|---|---|
|
016623b6a4 | ||
|
da60cce5d2 | ||
|
36fab5a203 | ||
|
bc30f84ccd | ||
|
85b9beb8f7 | ||
|
5894344c87 | ||
|
28b213df68 | ||
|
837da43dbb | ||
|
29cf4490e1 | ||
|
e9896d86d6 | ||
|
7675d6687b | ||
|
bfd60167fa | ||
|
85523e8321 | ||
|
4041b45cca | ||
|
083ef52e5d | ||
|
c6a5897337 |
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
|
||||
})
|
||||
}
|
@ -22,4 +22,18 @@ export async function userUpdate(data) {
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
export async function userCaptcha(data) {
|
||||
return await request( {
|
||||
url:'/api/v1/m/user/captcha',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
export async function userCaptchaValidate(data) {
|
||||
return await request( {
|
||||
url:'/mall/user/validate/captcha',
|
||||
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
|
||||
})
|
||||
}
|
258
app/components/puzzleComponent/index.vue
Normal file
258
app/components/puzzleComponent/index.vue
Normal file
@ -0,0 +1,258 @@
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||
//i18n
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const {t} =useI18n()
|
||||
const props = defineProps({
|
||||
options:Object,
|
||||
loading: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['leave'])
|
||||
|
||||
const moveX = ref(0)
|
||||
|
||||
const loaded = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const isVerifying = ref(false)
|
||||
const maxMoveX = ref(0)
|
||||
const bgImage = ref(null)
|
||||
|
||||
const verifyStatus = reactive({
|
||||
show: false,
|
||||
type: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
const dragState = reactive({
|
||||
startX: 0,
|
||||
oldMoveX: 0
|
||||
})
|
||||
|
||||
const onImageLoad = () => {
|
||||
if (!bgImage.value?.complete) return
|
||||
|
||||
const img = bgImage.value
|
||||
const scale = img.width / img.naturalWidth
|
||||
const blockSize = Math.round(50 * scale)
|
||||
maxMoveX.value = img.width - blockSize
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
const onImageError = () => {
|
||||
console.error('Image failed to load')
|
||||
maxMoveX.value = 270
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
// 拖动处理
|
||||
const startDrag = (e) => {
|
||||
isDragging.value = true
|
||||
dragState.startX = e.touches?.[0].clientX ?? e.clientX
|
||||
dragState.oldMoveX = moveX.value
|
||||
}
|
||||
|
||||
const onDrag = (e) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
const clientX = e.touches?.[0].clientX ?? e.clientX
|
||||
let newMoveX = dragState.oldMoveX + (clientX - dragState.startX)
|
||||
|
||||
moveX.value = Math.max(0, Math.min(newMoveX, maxMoveX.value))
|
||||
}
|
||||
|
||||
const endDrag = async () => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
isDragging.value = false
|
||||
isVerifying.value = true
|
||||
|
||||
try {
|
||||
emit('leave', moveX.value, (success) => {
|
||||
showVerifyResult(success)
|
||||
})
|
||||
} catch (error) {
|
||||
showVerifyResult(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证结果展示
|
||||
const showVerifyResult = (success) => {
|
||||
verifyStatus.show = true
|
||||
verifyStatus.type = success ? 'success' : 'error'
|
||||
verifyStatus.message = success ? t('components.form.verifySuccess') : t('components.form.verifyFailed')
|
||||
isVerifying.value = false
|
||||
setTimeout(() => {
|
||||
verifyStatus.show = false
|
||||
verifyStatus.message = ''
|
||||
moveX.value = 0
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 事件监听
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', onDrag)
|
||||
window.addEventListener('mouseup', endDrag)
|
||||
window.addEventListener('touchmove', onDrag)
|
||||
window.addEventListener('touchend', endDrag)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('mousemove', onDrag)
|
||||
window.removeEventListener('mouseup', endDrag)
|
||||
window.removeEventListener('touchmove', onDrag)
|
||||
window.removeEventListener('touchend', endDrag)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="m-auto bg-white p-15px rd-10px touch-none select-none">
|
||||
<div class="relative w-full overflow-hidden bg-#f8f8f8 rd-10px" :style="{ width: `${options?.canvasWidth}px`, height: `${options?.canvasHeight}px` }">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="absolute inset-0 flex flex-col items-center justify-center bg-#f8f8f8">
|
||||
<div class="fancy-loader">
|
||||
<div class="fancy-loader-bar"></div>
|
||||
<div class="fancy-loader-bar"></div>
|
||||
<div class="fancy-loader-bar"></div>
|
||||
<div class="fancy-loader-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 背景图 -->
|
||||
<img
|
||||
v-else
|
||||
:src="options?.canvasSrc"
|
||||
class="pointer-events-none w-full h-full"
|
||||
ref="bgImage"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
>
|
||||
|
||||
<!-- 滑块 -->
|
||||
<img
|
||||
:src="options?.blockSrc"
|
||||
class="absolute cursor-pointer will-change-transform transform-gpu"
|
||||
v-if="!loading"
|
||||
:class="{ 'transition-all duration-300 ease-out': !isDragging }"
|
||||
:style="{
|
||||
top: `${options?.blockY}px`,
|
||||
left: `${moveX}px`,
|
||||
visibility: loaded ? 'visible' : 'hidden',
|
||||
width: `${options?.blockWidth}px`, height: `${options?.blockHeight}px`
|
||||
}"
|
||||
>
|
||||
<transition name="fade-slide">
|
||||
<div
|
||||
v-if="verifyStatus.show"
|
||||
class="absolute left-0 bottom-0 w-full h-25px leading-25px text-center text-14px text-white"
|
||||
:class="verifyStatus.type === 'success' ? 'bg-#52c41a' : 'bg-#ff4d4f'"
|
||||
>
|
||||
{{ verifyStatus.message }}
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 滑动条 -->
|
||||
<div class="relative mt-15px h-40px">
|
||||
<div class="relative h-40px bg-#f5f5f5 rd-20px">
|
||||
<div
|
||||
class="absolute h-full bg-#91d5ff rd-20px"
|
||||
:class="{ 'transition-all duration-300 ease-out': !isDragging }"
|
||||
:style="{ width: `${moveX}px` }"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-0 w-40px h-40px bg-white rd-full shadow-[0_2px_6px_rgba(0,0,0,0.15)] cursor-pointer will-change-transform"
|
||||
:class="{ 'transition-all duration-300 ease-out': !isDragging }"
|
||||
:style="{ left: `${moveX}px` }"
|
||||
@mousedown.prevent="startDrag"
|
||||
@touchstart.prevent="startDrag"
|
||||
>
|
||||
<div
|
||||
class="absolute top-50% left-50% translate--50% w-20px h-20px bg-#1890ff rd-full"
|
||||
:class="{ 'animate-loading': isVerifying }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
@keyframes loading {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
.animate-loading {
|
||||
animation: loading 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 添加过渡动画样式 */
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from,
|
||||
.fade-slide-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 加载动画样式 */
|
||||
.fancy-loader {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.fancy-loader-bar {
|
||||
width: 6px;
|
||||
height: 15px;
|
||||
margin: 0 3px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 3px;
|
||||
animation: fancy-loading 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.fancy-loader-bar:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.fancy-loader-bar:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.fancy-loader-bar:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.fancy-loader-bar:nth-child(4) {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes fancy-loading {
|
||||
0% {
|
||||
transform: scaleY(0.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: scaleY(1.2);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(0.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* 保留原有的spin动画,用于其他地方 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -1,15 +1,22 @@
|
||||
<script setup>
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import Vcode from "vue3-puzzle-vcode";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import countryCode from '../countryRegion/data/index.js'
|
||||
import {senCode, userLogin} from "@/api/auth/index.js";
|
||||
import {senCode, userLogin,userCaptcha,userCaptchaValidate,} from "@/api/auth/index.js";
|
||||
import {authStore} from "@/stores/auth/index.js";
|
||||
import {message} from '@/components/x-message/useMessage.js'
|
||||
import {fddCheck} from "~/api/goods/index.js";
|
||||
import zu6020 from '@/static/images/zu6020@2x.png'
|
||||
|
||||
import PuzzleComponent from '@/components/puzzleComponent/index.vue'
|
||||
import { ref } from 'vue';
|
||||
const {userInfo,token,selectedZone}= authStore()
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { locale } = useI18n()
|
||||
const imgs=ref([zu6020])
|
||||
|
||||
definePageMeta({
|
||||
name: 'login',
|
||||
i18n: 'login.title'
|
||||
@ -17,6 +24,7 @@ definePageMeta({
|
||||
const loadingRef=ref({
|
||||
loading1:false,
|
||||
loading2:false,
|
||||
loading3:false,
|
||||
})
|
||||
const isExist=ref(false)//帐号是否存在 true存在
|
||||
const isReal=ref(false) //isReal 是否实名过
|
||||
@ -79,21 +87,31 @@ onMounted(()=>{
|
||||
selectedCountry.value=route.query.countryName || defaultCountry.name
|
||||
})
|
||||
const vanSwipeRef=ref(null)
|
||||
|
||||
const captcha=ref({
|
||||
nonceStr: "",
|
||||
blockX: 256 ,
|
||||
blockWidth:50,
|
||||
blockHeight:50,
|
||||
canvasWidth:320,
|
||||
canvasHeight:191,
|
||||
place:0,
|
||||
canvasSrc:'',
|
||||
blockSrc:'',
|
||||
blockY:0
|
||||
})
|
||||
const getCode =async () => {
|
||||
loadingRef.value.loading1=true
|
||||
const res=await senCode({
|
||||
telNum:phoneNum.value,
|
||||
zone:selectedZone.value
|
||||
})
|
||||
loadingRef.value.loading1=false
|
||||
isShow.value=true
|
||||
loadingRef.value.loading1=true
|
||||
const res=await userCaptcha(captcha.value)
|
||||
if (res.status===0){
|
||||
|
||||
|
||||
captcha.value.canvasSrc=`data:image/png;base64,${res.data.canvasSrc}`
|
||||
captcha.value.blockSrc=`data:image/png;base64,${res.data.blockSrc}`
|
||||
captcha.value.blockY=res.data.blockY
|
||||
captcha.value.nonceStr=res.data.nonceStr
|
||||
|
||||
loadingRef.value.loading1=false
|
||||
}
|
||||
pane.value = 1
|
||||
vanSwipeRef.value?.swipeTo(pane.value)
|
||||
|
||||
startCountdown();
|
||||
}
|
||||
const goBack = () => {
|
||||
code.value = ''
|
||||
@ -146,6 +164,43 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', () => {})
|
||||
})
|
||||
const isShow=ref(false)
|
||||
|
||||
const onLeave =async (moveX, callback) => {
|
||||
const res=await senCode({
|
||||
telNum:phoneNum.value,
|
||||
zone:selectedZone.value,
|
||||
verifyCaptcha:{
|
||||
blockX:moveX,
|
||||
nonceStr:captcha.value.nonceStr
|
||||
}
|
||||
})
|
||||
if (res.status===408){
|
||||
callback(false)
|
||||
getCode()
|
||||
message.warning(res.msg)
|
||||
}else if ([0,407].includes(res.status)){
|
||||
callback(true)
|
||||
if (res.status===407){
|
||||
message.warning(res.msg)
|
||||
}
|
||||
setTimeout(() => {
|
||||
pane.value = 1
|
||||
vanSwipeRef.value?.swipeTo(pane.value)
|
||||
startCountdown();
|
||||
isShow.value=false
|
||||
}, 1000)
|
||||
|
||||
}else {
|
||||
callback(true)
|
||||
setTimeout(() => {
|
||||
isShow.value=false
|
||||
},1000)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -174,7 +229,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[55px]">
|
||||
<van-button :loading="loadingRef.loading1" v-if="phoneNum" :loading-text="$t('login.getCode')" color="#2B53AC" block style="height: 48px" @click="getCode">{{ $t('login.getCode') }}</van-button>
|
||||
<van-button v-if="phoneNum" :loading-text="$t('login.getCode')" color="#2B53AC" block style="height: 48px" @click="getCode">{{ $t('login.getCode') }}</van-button>
|
||||
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">{{ $t('login.getCode') }}</van-button>
|
||||
</div>
|
||||
</div>
|
||||
@ -221,6 +276,15 @@ onUnmounted(() => {
|
||||
<div v-if="!isKeyboardVisible" class="text-center text-14px absolute left-1/2 transform translate-x--1/2 bottom-20px">
|
||||
{{ $t('login.agreement') }}<span class="text-#3454AF " @click="$router.push('/privacyPolicy')">{{ $t('login.privacyPolicy') }}</span>
|
||||
</div>
|
||||
<van-popup v-model:show="isShow" round style="max-width: initial" teleport="body">
|
||||
<PuzzleComponent
|
||||
v-if="isShow"
|
||||
:loading="loadingRef.loading1"
|
||||
:options="captcha"
|
||||
@leave="onLeave"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -238,4 +302,15 @@ onUnmounted(() => {
|
||||
width: 41px;
|
||||
height: 41px;
|
||||
}
|
||||
|
||||
.verify-popup-content {
|
||||
width: 90vw;
|
||||
max-width: 350px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.van-popup) {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
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 setupHttp1} from '@/api-collect-code/http'
|
||||
import { setupHttp as setupHttp2} from '@/api-public/http'
|
||||
export default defineNuxtPlugin(() => {
|
||||
setupHttp()
|
||||
setupHttp1()
|
||||
setupHttp2()
|
||||
})
|
||||
|
||||
|
BIN
app/static/images/reset.png
Normal file
BIN
app/static/images/reset.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
@ -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
|
||||
}
|
||||
}
|
@ -642,7 +642,9 @@
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel",
|
||||
"pleaseInput": "Please enter",
|
||||
"pleaseSelect": "Please select"
|
||||
"pleaseSelect": "Please select",
|
||||
"verifySuccess": "Verification successful",
|
||||
"verifyFailed": "Verification failed"
|
||||
},
|
||||
"upload": {
|
||||
"text": "Click to Upload",
|
||||
|
@ -642,7 +642,9 @@
|
||||
"submit": "送信",
|
||||
"cancel": "キャンセル",
|
||||
"pleaseInput": "入力してください",
|
||||
"pleaseSelect": "選択してください"
|
||||
"pleaseSelect": "選択してください",
|
||||
"verifySuccess": "認証成功",
|
||||
"verifyFailed": "認証失敗"
|
||||
},
|
||||
"upload": {
|
||||
"text": "アップロードするにはクリック",
|
||||
|
@ -646,7 +646,9 @@
|
||||
"submit": "提交",
|
||||
"cancel": "取消",
|
||||
"pleaseInput": "请输入",
|
||||
"pleaseSelect": "请选择"
|
||||
"pleaseSelect": "请选择",
|
||||
"verifySuccess": "验证成功",
|
||||
"verifyFailed": "验证失败"
|
||||
},
|
||||
"upload": {
|
||||
"text": "点击上传",
|
||||
|
@ -642,7 +642,9 @@
|
||||
"submit": "提交",
|
||||
"cancel": "取消",
|
||||
"pleaseInput": "請輸入",
|
||||
"pleaseSelect": "請選擇"
|
||||
"pleaseSelect": "請選擇",
|
||||
"verifySuccess": "驗證成功",
|
||||
"verifyFailed": "驗證失敗"
|
||||
},
|
||||
"upload": {
|
||||
"text": "點擊上傳",
|
||||
|
@ -809,8 +809,8 @@ packages:
|
||||
resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@11.1.1':
|
||||
resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==}
|
||||
'@intlify/shared@11.1.2':
|
||||
resolution: {integrity: sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/unplugin-vue-i18n@6.0.3':
|
||||
@ -5342,14 +5342,14 @@ snapshots:
|
||||
|
||||
'@intlify/shared@11.0.0-rc.1': {}
|
||||
|
||||
'@intlify/shared@11.1.1': {}
|
||||
'@intlify/shared@11.1.2': {}
|
||||
|
||||
'@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.20.1(jiti@2.4.2))(rollup@4.34.6)(typescript@5.7.3)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(jiti@2.4.2))
|
||||
'@intlify/bundle-utils': 10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))
|
||||
'@intlify/shared': 11.1.1
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
||||
'@intlify/shared': 11.1.2
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.34.6)
|
||||
'@typescript-eslint/scope-manager': 8.24.0
|
||||
'@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
|
||||
@ -5373,11 +5373,11 @@ snapshots:
|
||||
|
||||
'@intlify/utils@0.13.0': {}
|
||||
|
||||
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
|
||||
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.8
|
||||
optionalDependencies:
|
||||
'@intlify/shared': 11.1.1
|
||||
'@intlify/shared': 11.1.2
|
||||
'@vue/compiler-dom': 3.5.13
|
||||
vue: 3.5.13(typescript@5.7.3)
|
||||
vue-i18n: 10.0.5(vue@3.5.13(typescript@5.7.3))
|
||||
|
@ -1,4 +1,11 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["app/*"] // 确保这里的路径别名配置和你的项目配置一致
|
||||
}
|
||||
},
|
||||
"include": ["app/**/*"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user