Merge branch 'xingyy' into dev

This commit is contained in:
xingyy 2025-03-12 10:19:05 +08:00
commit 415267e307
39 changed files with 1327 additions and 22 deletions

125
app/api-public/http.js Normal file
View 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
}
}

View 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
})
}

View File

@ -23,3 +23,17 @@ export async function userUpdate(data) {
data 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
View 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
})
}

View File

@ -0,0 +1,367 @@
<template>
<div class="puzzle-container">
<div class="puzzle-box" :style="{ height: boxHeight + 'px' }">
<!-- 背景图 -->
<img :src="bgImageUrl" style="width: 320px;height: 191px;" ref="bgImage" @load="onImageLoad" @error="handleImageError">
<!-- 滑块 -->
<img
class="slider-block"
:src="sliderImageUrl"
:style="{
top: `${blockY}px`,
left: `${moveX}px`,
visibility: loaded ? 'visible' : 'hidden',
width: '50px',
height: '50px'
}"
></img>
<div v-if="verifySuccess || verifyError" :class="`text-#fff ${verifySuccess?'bg-#52C41A':'bg-#FF4D4F'} h-24px w-100% text-14px absolute left-0 bottom-0 text-center leading-24px`">{{ verifyTip }}</div>
</div>
<!-- 滑动条 -->
<div class="slider-container">
<div class="slider-track">
<div
class="slider-bar"
:style="{ width: `${moveX}px` }"
></div>
<div
class="slider-button"
:style="{ left: `${moveX}px` }"
@mousedown="handleMouseDown"
@touchstart="handleTouchStart"
>
<div class="slider-icon"></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps({
show: {
type: Boolean,
default: false
},
blockY: {
type: Number,
required: true
},
bgImageUrl: {
type: String,
required: true
},
sliderImageUrl: {
type: String,
required: true
}
})
const emit = defineEmits(['leave'])
//
const moveX = ref(0)
const startX = ref(0)
const oldMoveX = ref(0)
const maxMoveX = ref(0)
const boxHeight = ref(0)
const loaded = ref(false)
const isDragging = ref(false)
const verifySuccess = ref(false)
const verifyError = ref(false)
const verifyTip = ref('')
//
const reset = () => {
moveX.value = 0
verifySuccess.value = false
verifyError.value = false
verifyTip.value = ''
isDragging.value = false
}
// DOM
const bgImage = ref(null)
//
const onImageLoad = () => {
if (bgImage.value) {
try {
const img = bgImage.value
//
if (!img.complete) {
return
}
const scale = img.width / img.naturalWidth //
boxHeight.value = img.height
//
const blockSize = Math.round(50 * scale)
document.documentElement.style.setProperty('--block-size', blockSize + 'px')
maxMoveX.value = img.width - blockSize // 使
loaded.value = true
} catch (error) {
console.error('Image load error:', error)
//
const defaultBlockSize = 50
document.documentElement.style.setProperty('--block-size', defaultBlockSize + 'px')
maxMoveX.value = 320 - defaultBlockSize // 使
loaded.value = true
}
}
}
//
const handleImageError = () => {
console.error('Image failed to load')
//
const defaultBlockSize = 50
document.documentElement.style.setProperty('--block-size', defaultBlockSize + 'px')
maxMoveX.value = 320 - defaultBlockSize
loaded.value = true
}
const handleMouseDown = (event) => {
event.preventDefault()
startX.value = event.clientX
oldMoveX.value = moveX.value
isDragging.value = true
}
const handleTouchStart = (event) => {
event.preventDefault()
const touch = event.touches[0]
startX.value = touch.clientX
oldMoveX.value = moveX.value
isDragging.value = true
}
const move = (clientX) => {
let diff = clientX - startX.value
let newMoveX = oldMoveX.value + diff
//
if (newMoveX < 0) newMoveX = 0
if (newMoveX > maxMoveX.value) newMoveX = maxMoveX.value
moveX.value = Math.round(newMoveX) //
}
const handleMouseMove = (event) => {
if (!isDragging.value) return
move(event.clientX)
}
const handleTouchMove = (event) => {
if (!isDragging.value) return
event.preventDefault() //
move(event.touches[0].clientX)
}
const handleMouseUp = async () => {
if (!isDragging.value) return
isDragging.value = false
try {
emit('leave', moveX.value, (success) => {
if (success) {
verifySuccess.value = true
verifyError.value = false
verifyTip.value = '验证成功'
} else {
verifySuccess.value = false
verifyError.value = true
verifyTip.value = '验证失败'
}
})
} catch (error) {
verifySuccess.value = false
verifyError.value = true
verifyTip.value = '验证失败'
}finally{
setTimeout(() => {
reset()
}, 2000)
}
}
const handleTouchEnd = () => {
handleMouseUp()
}
//
const preloadImages = () => {
const bgImg = new Image()
const sliderImg = new Image()
bgImg.onload = () => {
if (bgImage.value) {
onImageLoad()
}
}
bgImg.src = props.bgImageUrl
sliderImg.src = props.sliderImageUrl
}
//
onMounted(() => {
preloadImages()
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
window.addEventListener('touchmove', handleTouchMove)
window.addEventListener('touchend', handleTouchEnd)
})
onBeforeUnmount(() => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
window.removeEventListener('touchmove', handleTouchMove)
window.removeEventListener('touchend', handleTouchEnd)
})
</script>
<style scoped>
:root {
--block-size: 50px;
}
.puzzle-container {
position: relative;
margin: 0 auto;
background: #fff;
padding: 15px;
border-radius: 10px;
touch-action: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
.puzzle-box {
position: relative;
width: 100%;
overflow: hidden;
background: #f8f8f8;
border-radius: 8px;
touch-action: none;
}
.bg-image {
display: block;
width: 100%;
height: auto;
-webkit-user-select: none;
user-select: none;
pointer-events: none;
object-fit: contain;
max-width: 100%;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
.slider-block {
position: absolute;
width: var(--block-size);
height: var(--block-size);
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
cursor: pointer;
-webkit-user-select: none;
user-select: none;
touch-action: none;
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
.slider-container {
position: relative;
margin-top: 15px;
height: 40px;
touch-action: none;
}
.slider-track {
position: relative;
height: 40px;
background: #f5f5f5;
border-radius: 20px;
touch-action: none;
}
.slider-bar {
position: absolute;
width: 100%;
height: 100%;
background: #91d5ff;
border-radius: 20px;
will-change: transform;
transform-origin: left;
}
.slider-button {
position: absolute;
top: 0;
width: 40px;
height: 40px;
background: #fff;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
cursor: pointer;
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
}
.slider-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
background: #1890ff;
border-radius: 50%;
}
.verify-result-bar {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 14px;
border-radius: 20px;
transition: all 0.3s;
z-index: 1;
}
.verify-result-bar.success {
background: #52c41a;
color: #fff;
}
.verify-result-bar.error {
background: #ff4d4f;
color: #fff;
}
/* 移除旧的提示样式 */
.verify-tip {
display: none;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,418 @@
<template>
<view class="slider-verify-box" v-if="isShow">
<view :style="`background-image: url('../../static/image/slider-verify/${
$i18n.locale
}/${isError ? 'bg_error' : 'bg'}.png');`" class="verifyBox">
<!-- <view class="slider-title">{{ $t('authentication.title') }}</view> -->
<image class="slider-verify-box-close" src="/static/image/icon/close.png" @click.stop="hideSliderBox">
</image>
<view class="slide-content">
<view class="slider-pintu">
<!-- <u-icon
name="reload"
size="32"
color="#fff"
class="reload"
@tap="refreshVerify"
v-if="!isLoading"
></u-icon> -->
<image src="../../static/image/slider-verify/reload.png" mode="widthFix" style="width: 38rpx"
class="reload" @tap="refreshVerify" v-if="!isLoading"></image>
<view class="load" v-if="isLoading">
<van-loading type="spinner" />
<!-- <u-loading-icon text="" textSize="16" :vertical="true"></u-loading-icon> -->
</view>
<template v-else>
<image id="pintuImg" :src="canvasSrc" class="pintu"></image>
<view class="pintukuai" :style="{ top: '0px', left: oldx + 'px' }">
<image :src="blockSrc" :style="{
top: blockY + 'px',
left: oldx + 'px',
width: blockWidth + 'px',
height: blockHeight + 'px'
}"></image>
</view>
<view class="mark" v-if="isMess">
<image :src="`../../static/image/slider-verify/${$i18n.locale}/error.png`" mode="widthFix"
v-if="isError"></image>
<image :src="`../../static/image/slider-verify/${$i18n.locale}/success.png`" mode="widthFix"
v-else></image>
</view>
</template>
</view>
<view class="slider-movearea" @touchend="endTouchMove">
<movable-area :animation="true">
<movable-view :class="
isLoading
? 'movable-view btn_info'
: isError
? 'movable-view btn_error'
: 'movable-view btn_success'
" :x="x" direction="horizontal" @change="startMove"></movable-view>
</movable-area>
<view class="huadao">{{
$t('authentication.content')
}}</view>
<view :class="
isError ? 'huadao_done error_bg' : 'huadao_done'
" :style="'width:' + doneWindth + 'px;'"></view>
</view>
</view>
</view>
</view>
</template>
<script>
import {
postDataByParams
} from '../../utils/api.js'
export default {
name: 'slider-verify',
props: {
isShow: {
type: Boolean,
default: true
}
},
data() {
return {
x: 0, //
oldx: 0, //
top: 0, //top
canvasSrc: '',
blockSrc: '',
blockY: '',
nonceStr: '',
blockWidth: 50, //(blockWidth14)
blockHeight: 50,
isLoading: true,
doneWindth: 0,
isError: false,
isMess: false
}
},
watch: {
//
isShow(newValue, oldValue) {
if (newValue) {
this.refreshVerify() //
}
}
},
mounted() {
var that = this
// that.refreshVerify();
that.getCaptcha()
// console.log(this.$i18n.locale, 'this.$i18n.locale')
},
methods: {
async getCaptcha() {
this.isLoading = true
var that = this
let url = 'generate/captcha'
let params = {
canvasWidth: 320, //(canvasWidth41canvasWidth-10/2 - 1> blockWidth)
canvasHeight: 190, //(canvasHeight26 canvasHeight - blockHeight > 11
blockWidth: this.blockWidth, //(blockWidth14)
blockHeight: this.blockHeight, //(blockHeight14)
// blockRadius: 9, //
place: 0 //0URL1 0
}
postDataByParams(url, params)
.then((res) => {
if (res.status === 0) {
that.canvasSrc = 'data:image/jpg;base64,' + res.data.canvasSrc
that.blockSrc = 'data:image/jpg;base64,' + res.data.blockSrc
that.blockY = res.data.blockY
that.nonceStr = res.data.nonceStr
that.isLoading = false
} else {
this.$emit('sliderError', encodeURIComponent(JSON.stringify(res)))
}
})
.catch((err) => {
this.$emit('sliderError', encodeURIComponent(JSON.stringify(err)), true)
});
},
//
refreshVerify() {
this.x = 1
this.oldx = 1
setTimeout(() => {
this.x = 0
this.oldx = 0
this.doneWindth = 0
}, 300)
this.getCaptcha()
this.isError = false
this.isMess = false
},
/* 滑动中 */
startMove(e) {
// console.log(e.detail.x)
this.oldx = e.detail.x / 2 - 5
if (e.detail.x > 1) {
this.doneWindth = e.detail.x + 30
} else {
this.doneWindth = 0
}
},
/* 滑动结束 */
async endTouchMove() {
var that = this
// console.log('')
let url = 'validate/captcha'
let params = {
nonceStr: this.nonceStr,
blockX: this.oldx * 2 + 5
}
postDataByParams(url, params)
.then((res) => {
this.isMess = true
if (res.status == 0) {
setTimeout(() => {
that.$emit('touchSliderResult', res.data.nonceStr)
}, 1000)
} else {
this.isError = true
this.$emit('sliderError', encodeURIComponent(JSON.stringify(res)))
}
})
.catch((err) => {
this.$emit('sliderError', encodeURIComponent(JSON.stringify(err)), true)
});
},
/* 重置阴影位置 */
/* resetMove() {
this.x = 1;
this.oldx = 1;
setTimeout(() => {
this.x = 0;
this.oldx = 0;
}, 300);
}, */
//
closeSlider() {
this.$emit('touchSliderResult', false)
},
//
hideSliderBox() {
this.$emit('hideSliderBox')
}
}
}
</script>
<style lang="scss">
.slider-verify-box {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.error_box {
// background: url('../../static/image/slider-verify/bg_error.png') no-repeat !important;
background-size: 100% 100% !important;
}
.verifyBox {
// width: 588rpx;
// height: 662rpx;
padding: 218rpx 45rpx 30rpx 45rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
// width: 85%;
// background-color: #fff;
background-repeat: no-repeat;
// background: url('../../static/image/slider-verify/bg.png') no-repeat;
background-size: 100% 100%;
border-radius: 20upx;
// box-shadow: 0 0 5upx rgba(0, 0, 0, 1);
.slider-verify-box-close {
width: 40rpx;
height: 40rpx;
position: absolute;
top: 0;
right: 0;
}
.slider-title {
font-size: 36upx;
text-align: center;
padding: 12rpx 0;
color: #000000;
}
.slide-content {
// width: 560rpx;
// padding: 0 ;
// margin: 0 auto;
.slide-tips {
font-size: 28rpx;
color: rgba(2, 20, 33, 0.45);
padding: 0.5em 0;
}
.slider-pintu {
position: relative;
width: 100%;
border-radius: 10rpx;
overflow: hidden;
.reload {
position: absolute;
right: 10rpx;
top: 10rpx;
z-index: 110;
}
.load {
width: 320px;
height: 190px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
background: #000000;
}
.pintu {
width: 320px;
height: 190px;
display: block;
margin: 0 auto;
}
.pintukuai {
position: absolute;
/* top: 0;
left: 0; */
z-index: 100;
box-shadow: 0 0 5upx rgba(0, 0, 0, 0.3);
image {
display: block;
position: absolute;
top: 0;
left: 0;
/* width: 560rpx;
height: 315rpx; */
}
}
.mark {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #000000;
z-index: 101;
display: flex;
align-items: center;
justify-content: center;
image {
width: 60%;
height: 50rpx;
}
}
}
.yinying {
position: absolute;
width: 120rpx;
height: 120rpx;
background-color: rgba(0, 0, 0, 0.5);
}
}
}
.slider-movearea {
position: relative;
height: 80upx;
width: 320px;
margin-top: 25rpx;
movable-area {
width: 100%;
height: 64rpx;
.movable-view {
width: 100upx;
height: 74rpx;
border-radius: 40upx;
// background-color: #699A70;
background-image: url(../../static/image/slider-verify/icon-button-normal.png);
background-repeat: no-repeat;
background-size: auto 30upx;
background-position: center;
position: relative;
z-index: 100;
border: 1px solid transparent;
}
}
}
.btn_info {
background-color: #878787 !important;
}
.btn_error {
background-color: #fd343c !important;
}
.btn_success {
background-color: #699a70 !important;
}
.error_bg {
background-color: #ffb7ba !important;
}
.success_bg {
background-color: #aad0b0 !important;
}
.huadao {
width: 100%;
// width: 320px;
height: 66upx;
line-height: 66upx;
background: #ededed;
// box-shadow: inset 0 0 5upx #EDEDED;
border-radius: 40rpx;
color: #bcbcbc;
text-align: center;
box-sizing: border-box;
position: absolute;
top: 7rpx;
left: 0;
font-size: 28rpx;
z-index: 99;
}
.huadao_done {
height: 66upx;
line-height: 66upx;
background: #aad0b0;
border-radius: 40rpx;
text-align: center;
box-sizing: border-box;
position: absolute;
top: 7rpx;
left: 0;
font-size: 28rpx;
z-index: 99;
}
</style>

View File

@ -1,15 +1,20 @@
<script setup> <script setup>
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import Vcode from "vue3-puzzle-vcode";
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import countryCode from '../countryRegion/data/index.js' 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 {authStore} from "@/stores/auth/index.js";
import {message} from '@/components/x-message/useMessage.js' import {message} from '@/components/x-message/useMessage.js'
import {fddCheck} from "~/api/goods/index.js"; import {fddCheck} from "~/api/goods/index.js";
import zu6020 from '@/static/images/zu6020@2x.png'
import YourPuzzleComponent from '@/components/YourPuzzleComponent.vue'
const {userInfo,token,selectedZone}= authStore() const {userInfo,token,selectedZone}= authStore()
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const { locale } = useI18n() const { locale } = useI18n()
const imgs=ref([zu6020])
definePageMeta({ definePageMeta({
name: 'login', name: 'login',
i18n: 'login.title' i18n: 'login.title'
@ -79,21 +84,45 @@ onMounted(()=>{
selectedCountry.value=route.query.countryName || defaultCountry.name selectedCountry.value=route.query.countryName || defaultCountry.name
}) })
const vanSwipeRef=ref(null) const vanSwipeRef=ref(null)
const captcha=ref({
"nonceStr": "397b5e08168c31c4",
"blockX": 256
})
const captchaUrl=ref('')
const captchaVerifyUrl=ref('')
const blockY=ref(0)
const getCode =async () => { const getCode =async () => {
loadingRef.value.loading1=true loadingRef.value.loading1=true
const res=await senCode({ const res=await userCaptcha({
telNum:phoneNum.value, "canvasWidth": 320, //(canvasWidth41canvasWidth-10/2 - 1> blockWidth)
zone:selectedZone.value "canvasHeight": 191, //(canvasHeight26 canvasHeight - blockHeight > 11
}) "blockWidth": 50, //(blockWidth14)
loadingRef.value.loading1=false "blockHeight": 50, //(blockHeight14)
// "blockRadius": 25, //
"place": 0 //0URL1 0
})
if (res.status===0){ if (res.status===0){
captchaUrl.value=`data:image/png;base64,${res.data.canvasSrc}`
captchaVerifyUrl.value=`data:image/png;base64,${res.data.blockSrc}`
blockY.value=res.data.blockY
captcha.value.nonceStr=res.data.nonceStr
isShow.value=true
loadingRef.value.loading1=false
} }
pane.value = 1 // loadingRef.value.loading1=true
vanSwipeRef.value?.swipeTo(pane.value) // const res=await senCode({
// telNum:phoneNum.value,
// zone:selectedZone.value
// })
// loadingRef.value.loading1=false
// if (res.status===0){
startCountdown();
// }
// pane.value = 1
// vanSwipeRef.value?.swipeTo(pane.value)
// startCountdown();
} }
const goBack = () => { const goBack = () => {
code.value = '' code.value = ''
@ -146,6 +175,41 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', () => {}) window.removeEventListener('resize', () => {})
}) })
const isShow=ref(false)
const onSuccess=()=>{
// userCaptchaValidate()
isShow.value=false
}
const onClose=()=>{
isShow.value=false
}
const onLeave =async (moveX, callback) => {
loadingRef.value.loading1=true
const res=await senCode({
telNum:phoneNum.value,
zone:selectedZone.value,
verifyCaptcha:{
blockX:moveX,
nonceStr:captcha.value.nonceStr
}
})
loadingRef.value.loading1=false
if (res.status===408){
callback(false)
getCode()
}else{
callback(true)
setTimeout(() => {
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
startCountdown();
isShow.value=false
}, 1000)
}
}
</script> </script>
<template> <template>
@ -221,6 +285,15 @@ onUnmounted(() => {
<div v-if="!isKeyboardVisible" class="text-center text-14px absolute left-1/2 transform translate-x--1/2 bottom-20px"> <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> {{ $t('login.agreement') }}<span class="text-#3454AF " @click="$router.push('/privacyPolicy')">{{ $t('login.privacyPolicy') }}</span>
</div> </div>
<van-popup v-model:show="isShow" round>
<YourPuzzleComponent
:blockY="blockY"
:bgImageUrl="captchaUrl"
:sliderImageUrl="captchaVerifyUrl"
@leave="onLeave"
/>
</van-popup>
</div> </div>
</template> </template>
@ -238,4 +311,15 @@ onUnmounted(() => {
width: 41px; width: 41px;
height: 41px; height: 41px;
} }
.verify-popup-content {
width: 90vw;
max-width: 350px;
padding: 20px;
box-sizing: border-box;
}
:deep(.van-popup) {
background: transparent;
}
</style> </style>

View 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>

View 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>

View File

@ -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()
}) })

BIN
app/static/images/reset.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -248,6 +248,7 @@ export const liveStore = createGlobalState(() => {
} }
} }
return{ return{
decryptUtils,
wsClient, wsClient,
fullLive, fullLive,
isMinWindow, isMinWindow,

View 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
View 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
}
}

View File

@ -38,7 +38,8 @@
"vue-demi": "^0.14.10", "vue-demi": "^0.14.10",
"vue-pdf-embed": "^2.1.2", "vue-pdf-embed": "^2.1.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vue-signature-pad": "^3.0.2" "vue-signature-pad": "^3.0.2",
"vue3-puzzle-vcode": "1.1.6-nuxt"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/carbon": "^1.2.5", "@iconify-json/carbon": "^1.2.5",

View File

@ -77,6 +77,9 @@ importers:
vue-signature-pad: vue-signature-pad:
specifier: ^3.0.2 specifier: ^3.0.2
version: 3.0.2(vue@3.5.13(typescript@5.7.3)) version: 3.0.2(vue@3.5.13(typescript@5.7.3))
vue3-puzzle-vcode:
specifier: 1.1.6-nuxt
version: 1.1.6-nuxt
devDependencies: devDependencies:
'@iconify-json/carbon': '@iconify-json/carbon':
specifier: ^1.2.5 specifier: ^1.2.5
@ -809,8 +812,8 @@ packages:
resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==} resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
'@intlify/shared@11.1.1': '@intlify/shared@11.1.2':
resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==} resolution: {integrity: sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
'@intlify/unplugin-vue-i18n@6.0.3': '@intlify/unplugin-vue-i18n@6.0.3':
@ -4618,6 +4621,9 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.2.0 vue: ^3.2.0
vue3-puzzle-vcode@1.1.6-nuxt:
resolution: {integrity: sha512-V3DrPIYznxko8jBAtZtmsNPw9QmkPnFicQ0p9B192vC3ncRv4IDazhLC7D/cY/OGq0OeqXmk2DiOcBR7dyt8GQ==}
vue@3.5.13: vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
peerDependencies: peerDependencies:
@ -5342,14 +5348,14 @@ snapshots:
'@intlify/shared@11.0.0-rc.1': {} '@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))': '@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: dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1(jiti@2.4.2)) '@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/bundle-utils': 10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.7.3)))
'@intlify/shared': 11.1.1 '@intlify/shared': 11.1.2
'@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))
'@rollup/pluginutils': 5.1.4(rollup@4.34.6) '@rollup/pluginutils': 5.1.4(rollup@4.34.6)
'@typescript-eslint/scope-manager': 8.24.0 '@typescript-eslint/scope-manager': 8.24.0
'@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
@ -5373,11 +5379,11 @@ snapshots:
'@intlify/utils@0.13.0': {} '@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: dependencies:
'@babel/parser': 7.26.8 '@babel/parser': 7.26.8
optionalDependencies: optionalDependencies:
'@intlify/shared': 11.1.1 '@intlify/shared': 11.1.2
'@vue/compiler-dom': 3.5.13 '@vue/compiler-dom': 3.5.13
vue: 3.5.13(typescript@5.7.3) vue: 3.5.13(typescript@5.7.3)
vue-i18n: 10.0.5(vue@3.5.13(typescript@5.7.3)) vue-i18n: 10.0.5(vue@3.5.13(typescript@5.7.3))
@ -9896,6 +9902,8 @@ snapshots:
signature_pad: 3.0.0-beta.4 signature_pad: 3.0.0-beta.4
vue: 3.5.13(typescript@5.7.3) vue: 3.5.13(typescript@5.7.3)
vue3-puzzle-vcode@1.1.6-nuxt: {}
vue@3.5.13(typescript@5.7.3): vue@3.5.13(typescript@5.7.3):
dependencies: dependencies:
'@vue/compiler-dom': 3.5.13 '@vue/compiler-dom': 3.5.13

View File

@ -1,4 +1,11 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript // https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json" "extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["app/*"] //
}
},
"include": ["app/**/*"]
} }