Merge branch 'xingyy' into dev

# Conflicts:
#	app/components/AppFooter.vue
This commit is contained in:
xingyy 2025-02-11 15:40:38 +08:00
commit e72ce59fca
130 changed files with 8855 additions and 3637 deletions

1
.env
View File

@ -1 +0,0 @@
NUXT_PUBLIC_API_BASE=https://easyapi.devv.zone

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Charlie Wang ✨
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -39,9 +39,6 @@
- 🔥 `<script setup>` 语法
- 🌍 [国际化支持](./i18n/locales)
- 🍍 [使用 Pinia 进行状态管理](https://github.com/vuejs/pinia),查看 [./app/composables/counter.ts](./app/composables/counter.ts)
- 📑 [布局系统](./app/layouts)
- 📥 API 自动导入 - 用于 Composition API 和自定义组合式函数
@ -57,8 +54,6 @@
- [i18n](https://github.com/nuxt-modules/i18n) - Nuxt 的国际化模块
- [ColorMode](https://github.com/nuxt-modules/color-mode) - 支持自动检测的深色和浅色模式
- [UnoCSS](https://github.com/unocss/unocss) - 即时按需原子化 CSS 引擎
- [Pinia](https://github.com/vuejs/pinia) - 直观、类型安全、轻量且灵活的 Vue 状态管理
- [Pinia Persistedstate](https://github.com/prazdevs/pinia-plugin-persistedstate) - 可配置的 Pinia 存储持久化和重新激活
- [DevTools](https://github.com/nuxt/devtools) - 释放 Nuxt 开发者体验
## IDE

View File

@ -0,0 +1,23 @@
import { request } from '@/api/http.js'
export async function checkPhone(data) {
return await request({
url:'/api/v1/common/check/phone',
method: 'POST',
data
})
}
export async function userSend(data) {
return await request( {
url:'/api/v1/m/user/send',
method: 'POST',
data
})
}
export async function mobileLogin(data) {
return await request( {
url:'/api/v1/m/user/mobile/login',
method: 'POST',
data
})
}

View File

@ -0,0 +1,31 @@
import { request } from '@/api/http.js'
export async function offlineQrcodeList(data) {
return await request( {
url:'/api/v1/offlineQrcode/query',
method: 'POST',
data
})
}
export async function offlineQrcodeCreate(data) {
return await request ({
url:'/api/v1/offlineQrcode/create',
method: 'POST',
data
})
}
export async function offlineQrcodeDelete(data) {
return await request ({
url:'/api/v1/offlineQrcode/delete',
method: 'POST',
data
})
}
export async function userArtworks(data) {
return await request( {
url:'/api/v1/m/user/artworks',
method: 'POST',
data
})
}

View File

@ -0,0 +1,128 @@
import {useRuntimeConfig} from '#app'
import {ofetch} from 'ofetch'
import {message} from '@/components/x-message/useMessage.js'
import {codeAuthStore} from "@/stores-collect-code/auth/index.js"
let httpStatusErrorHandler
let http
// HTTP 状态码映射
const HTTP_STATUS_MAP = {
400: '请求参数错误',
401: '未授权或登录过期',
403: '访问被禁止',
404: '请求的资源不存在',
500: '服务器内部错误',
502: '网关错误',
503: '服务暂时不可用',
504: '网关超时'
}
export function setupHttp() {
if (http) return http
const {token}= codeAuthStore()
const config = useRuntimeConfig()
const baseURL = config.public.NUXT_PUBLIC_API_COLLECT_CODE
const router = useRouter()
const defaultOptions = {
baseURL,
headers: { 'Content-Type': 'application/json' },
timeout: 15000, // 15秒超时
retry: 3,
retryDelay: 1000,
}
http = ofetch.create({
...defaultOptions,
// 请求拦截
async onRequest({ options, request }) {
// 添加 token
options.headers = {
...options.headers,
Authorization: token.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 || '操作失败')
}
// 处理登录失效
if (data.status === 401) {
message.error('登录已过期,请重新登录')
token.value = '' // 清除 token
router.replace('/collectCode/login')
}
return response
},
// 响应错误处理
async onResponseError({ response, request }) {
// 网络错误
if (!response) {
message.error('网络连接失败,请检查网络设置')
return Promise.reject(new Error('网络错误'))
}
const status = response.status
const data = response._data
// 处理 HTTP 状态错误
const errorMessage = data.msg || HTTP_STATUS_MAP[status] || '请求失败'
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('HTTP client not initialized. Call setupHttp first.')
}
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/auth/index.js Normal file
View File

@ -0,0 +1,25 @@
import { request } from '@/api/http.js'
export async function senCode(data) {
return await request({
url:'/api/v1/m/user/send',
method: 'POST',
data
})
}
export async function userLogin(data) {
return await request( {
url:'/api/v1/m/user/login',
method: 'POST',
data
})
}
export async function userUpdate(data) {
return await request( {
url:'/api/v1/m/user/update',
method: 'POST',
data
})
}

56
app/api/goods/index.js Normal file
View File

@ -0,0 +1,56 @@
import { request } from '@/api/http.js'
export async function artworkList(data) {
return await request( {
url:'/api/v1/m/auction/default/artwork/list',
method: 'POST',
data
})
}
export async function defaultDetail(data) {
return await request ({
url:'/api/v1/m/auction/default/detail',
method: 'POST',
data
})
}
export async function artworkDetail(data) {
return await request( {
url:'/api/v1/m/artwork/detail',
method: 'POST',
data,
})
}
export async function userArtworks(data) {
return await request( {
url:'/api/v1/m/user/artworks',
method: 'POST',
data
})
}
export async function userArtwork(data) {
return await request( {
url:'/api/v1/m/user/artwork',
method: 'POST',
data
})
}
export async function artworkBuy(data) {
return await request( {
url:'/api/v1/m/artwork/buy',
method: 'POST',
data
})
}
export async function logSendlog(data) {
return await request( {
url:'/api/v1/m/auction/log/sendlog',
method: 'POST',
data
})
}

128
app/api/http.js Normal file
View File

@ -0,0 +1,128 @@
import {useRuntimeConfig} from '#app'
import {ofetch} from 'ofetch'
import {message} from '@/components/x-message/useMessage.js'
import {authStore} from "@/stores/auth/index.js"
let httpStatusErrorHandler
let http
// HTTP 状态码映射
const HTTP_STATUS_MAP = {
400: '请求参数错误',
401: '未授权或登录过期',
403: '访问被禁止',
404: '请求的资源不存在',
500: '服务器内部错误',
502: '网关错误',
503: '服务暂时不可用',
504: '网关超时'
}
export function setupHttp() {
if (http) return http
const config = useRuntimeConfig()
const baseURL = config.public.NUXT_PUBLIC_API_BASE
const { token } = authStore()
const router = useRouter()
const defaultOptions = {
baseURL,
headers: { 'Content-Type': 'application/json' },
timeout: 15000, // 15秒超时
retry: 3,
retryDelay: 1000,
}
http = ofetch.create({
...defaultOptions,
// 请求拦截
async onRequest({ options, request }) {
// 添加 token
options.headers = {
...options.headers,
Authorization: token.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 || '操作失败')
}
// 处理登录失效
if (data.status === 401) {
message.error('登录已过期,请重新登录')
token.value = '' // 清除 token
router.replace('/login')
}
return response
},
// 响应错误处理
async onResponseError({ response, request }) {
// 网络错误
if (!response) {
message.error('网络连接失败,请检查网络设置')
return Promise.reject(new Error('网络错误'))
}
const status = response.status
const data = response._data
// 处理 HTTP 状态错误
const errorMessage = data.msg || HTTP_STATUS_MAP[status] || '请求失败'
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('HTTP client not initialized. Call setupHttp first.')
}
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

@ -1,55 +0,0 @@
import type { $Fetch } from 'ofetch'
import { useRuntimeConfig } from '#app'
import { ofetch } from 'ofetch'
type HttpStatusErrorHandler = (message: string, statusCode: number) => void
let httpStatusErrorHandler: HttpStatusErrorHandler
let http: $Fetch
export function setupHttp() {
if (http)
return http
const config = useRuntimeConfig()
const baseURL = config.public.apiBase as string
http = ofetch.create({
baseURL,
headers: { 'Content-Type': 'application/json' },
async onRequest({ options }) {
const token = localStorage.getItem('token')
options.headers = {
...options.headers,
...(token && { Authorization: `Bearer ${token}` }),
}
},
async onResponseError({ response }) {
const { message } = response._data
if (Array.isArray(message)) {
message.forEach((item) => {
httpStatusErrorHandler?.(item, response.status)
})
}
else {
httpStatusErrorHandler?.(message, response.status)
}
return Promise.reject(response._data)
},
retry: 3,
retryDelay: 1000,
})
}
export function injectHttpStatusErrorHandler(handler: HttpStatusErrorHandler) {
httpStatusErrorHandler = handler
}
export function getHttp() {
if (!http) {
throw new Error('HTTP client not initialized. Call setupHttp first.')
}
return http
}

View File

@ -1,8 +0,0 @@
import { getHttp } from './http'
export async function getProse() {
const http = getHttp()
return await http('/api/prose', {
method: 'GET',
})
}

View File

@ -1,28 +1,86 @@
<script setup lang="ts">
import type { ConfigProviderTheme } from 'vant'
import useKeepalive from '~/composables/keepalive'
import { appName } from '~/constants'
<script setup>
import {useI18n} from 'vue-i18n'
import {message} from '@/components/x-message/useMessage.js'
// message.success('success')
useHead({
title: appName,
title: useI18n().t('appSetting.appName'),
meta: [
{name: 'description', content: useI18n().t('appSetting.appDescription')},
{name: 'keywords', content: useI18n().t('appSetting.appKeyWords')},
],
})
const color = useColorMode()
const mode = computed(() => {
return color.value as ConfigProviderTheme
//
const router = useRouter()
const route = useRoute()
const slideDirection = ref('slide-left')
//
const routeHistory = ref([])
router.beforeEach((to, from) => {
//
routeHistory.value.push(from.path)
//
if (routeHistory.value.includes(to.path)) {
slideDirection.value = 'slide-right'
//
const index = routeHistory.value.indexOf(to.path)
routeHistory.value = routeHistory.value.slice(0, index)
} else {
slideDirection.value = 'slide-left'
}
})
const keepAliveRouteNames = computed(() => {
return useKeepalive().routeCaches as string[]
})
//
provide('slideDirection', slideDirection)
</script>
<template>
<VanConfigProvider :theme="mode">
<NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
<VanConfigProvider>
<NuxtLoadingIndicator
color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)"/>
<NuxtLayout>
<NuxtPage :keepalive="{ include: keepAliveRouteNames }" />
<NuxtPage :transition="{
name: slideDirection
}"/>
</NuxtLayout>
</VanConfigProvider>
</template>
<style>
:root:root {
--van-dialog-radius: 8px
}
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: absolute;
width: 100%;
}
.slide-left-enter-from {
transform: translateX(100%);
}
.slide-left-leave-to {
transform: translateX(-100%);
}
.slide-right-enter-from {
transform: translateX(-100%);
}
.slide-right-leave-to {
transform: translateX(100%);
}
:root {
--safe-area-inset-bottom: env(safe-area-inset-bottom);
}
</style>

View File

@ -1,30 +1,38 @@
<script setup lang="ts">
import { useAppFooterRouteNames as names } from '~/config'
<script setup>
import { useAppFooterRouteNames as names } from '@/config/index.js'
import MyIcon from "@/components/icons/MyIcon.vue";
import HomeIcon from "@/components/icons/HomeIcon.vue";
const route = useRoute()
const active = ref(0)
const show = computed(() => {
if (route.name && names.includes(route.name))
return true
return false
})
const initData=()=>{
active.value=route.path==='/profile'?1:0
}
watchEffect(initData)
onMounted(()=>{
initData()
})
</script>
<template>
<van-tabbar v-if="show" v-model="active" route placeholder fixed>
<van-tabbar-item replace to="/">
<span>{{ $t('tabbar.home') }}</span>
<template #icon>
<div class="i-carbon:home" />
</template>
</van-tabbar-item>
<van-tabbar-item replace to="/profile">
<span>{{ $t('tabbar.profile') }}</span>
<template #icon>
<div class="i-carbon:user" />
</template>
</van-tabbar-item>
</van-tabbar>
<div v-if="show" v-memo="[active]" >
<van-tabbar v-model="active" route placeholder fixed>
<van-tabbar-item replace to="/">
<span>{{ $t('tabbar.home') }}</span>
<template #icon>
<HomeIcon :active="active===0"></HomeIcon>
</template>
</van-tabbar-item>
<van-tabbar-item replace to="/profile">
<span>{{ $t('tabbar.profile') }}</span>
<template #icon>
<MyIcon :active="active===1"></MyIcon>
</template>
</van-tabbar-item>
</van-tabbar>
</div>
</template>

View File

@ -1,10 +1,14 @@
<script setup lang="ts">
import { useAppFooterRouteNames as routeWhiteList } from '~/config'
<script setup>
import { useAppHeaderRouteNames as routeWhiteList } from '@/config'
import { liveStore } from "@/stores/live/index.js";
const { fullLive } = liveStore()
const route = useRoute()
const router = useRouter()
function onBack() {
if (fullLive.value){
fullLive.value=false
return
}
if (window.history.state.back)
history.back()
else
@ -14,19 +18,34 @@ function onBack() {
const { t } = useI18n()
const title = computed(() => {
if (!route.meta)
return ''
return route.meta.i18n ? t(route.meta.i18n) : (route.meta.title || '')
})
const subTitle = computed(() => {
if (!route.meta)
return ''
return route.meta.subTitle ? t(route.meta.subTitle) : ''
})
const showLeftArrow = computed(() => route.name && routeWhiteList.includes(route.name))
</script>
<template>
<VanNavBar
v-memo="[title,fullLive,showLeftArrow,subTitle]"
:title="title"
:left-arrow="!showLeftArrow"
:left-arrow="!showLeftArrow||fullLive"
placeholder clickable fixed
@click-left="onBack"
/>
>
<template #title v-if="route.meta.i18n==='menu.goods'">
<div class="flex flex-col items-center justify-center">
<div class="text-#000000 text-17px mb-5px font-600">{{ title }}</div>
<div class="text-#939393 text-10px line-height-none font-100">{{subTitle}}</div>
</div>
</template>
</VanNavBar>
</template>

View File

@ -0,0 +1,181 @@
<template>
<div class="signature-pad-container">
<canvas
ref="canvasRef"
class="signature-pad"
:style="{
width: '100%',
height: '100%',
backgroundColor: '#fff',
border: '1px solid #e5e5e5',
borderRadius: '4px'
}"
@touchstart="handleStart"
@touchmove="handleMove"
@touchend="handleEnd"
@mousedown="handleStart"
@mousemove="handleMove"
@mouseup="handleEnd"
@mouseleave="handleEnd"
></canvas>
<div class="signature-controls">
<van-button
type="default"
size="small"
@click="clearCanvas"
>清除</van-button>
<van-button
type="primary"
size="small"
@click="handleConfirm"
>确认</van-button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const canvasRef = ref(null)
const ctx = ref(null)
const isDrawing = ref(false)
const lastX = ref(0)
const lastY = ref(0)
const LINE_WIDTH = 2 //
//
const initCanvas = () => {
const canvas = canvasRef.value
const dpr = window.devicePixelRatio || 1
const rect = canvas.getBoundingClientRect()
//
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
ctx.value = canvas.getContext('2d')
//
ctx.value.scale(dpr, dpr)
ctx.value.lineCap = 'round'
ctx.value.lineJoin = 'round'
ctx.value.strokeStyle = '#000'
ctx.value.lineWidth = LINE_WIDTH
}
//
const handleStart = (e) => {
e.preventDefault() //
isDrawing.value = true
const point = getPoint(e)
lastX.value = point.x
lastY.value = point.y
}
//
const handleMove = (e) => {
if (!isDrawing.value) return
e.preventDefault() //
const point = getPoint(e)
ctx.value.beginPath()
ctx.value.moveTo(lastX.value, lastY.value)
ctx.value.lineTo(point.x, point.y)
ctx.value.stroke()
lastX.value = point.x
lastY.value = point.y
}
//
const handleEnd = () => {
isDrawing.value = false
}
//
const getPoint = (e) => {
const canvas = canvasRef.value
const rect = canvas.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
const event = e.touches ? e.touches[0] : e
//
const x = (event.clientX - rect.left)
const y = (event.clientY - rect.top)
return {
x: x,
y: y
}
}
//
const clearCanvas = () => {
const canvas = canvasRef.value
ctx.value.clearRect(0, 0, canvas.width, canvas.height)
emit('update:modelValue', '')
emit('change', '')
}
//
const handleConfirm = () => {
const canvas = canvasRef.value
const imageData = canvas.toDataURL('image/png')
emit('update:modelValue', imageData)
emit('change', imageData)
}
//
const handleResize = () => {
const canvas = canvasRef.value
const imageData = canvas.toDataURL('image/png')
initCanvas()
//
const img = new Image()
img.onload = () => {
ctx.value.drawImage(img, 0, 0, canvas.width, canvas.height)
}
img.src = imageData
}
onMounted(() => {
initCanvas()
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.signature-pad-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
.signature-pad {
flex: 1;
touch-action: none;
}
.signature-controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
padding: 8px;
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<div>
<img v-if="active" src="./images/home1.png" alt="" class="w-[18px] h-[20px]">
<img v-else src="./images/home2.png" alt="" class="w-[18px] h-[20px]">
</div>
</template>
<script setup>
defineProps({
active: {
type: Boolean,
default: false
}
})
</script>

View File

@ -0,0 +1,15 @@
<template>
<div>
<img v-if="active" src="./images/my1.png" alt="" class="w-[18px] h-[20px]">
<img v-else src="./images/my2.png" alt="" class="w-[18px] h-[20px]">
</div>
</template>
<script setup>
defineProps({
active: {
type: Boolean,
default: false
}
})
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,55 @@
<script setup>
import { showImagePreview } from 'vant';
import xImage from '@/components/x-image/index.vue'
const props = defineProps({
detailInfo: {
type: Object,
default: null
}
})
</script>
<template>
<div>
<div class="flex justify-center">
<xImage class="h-188px" :src="detailInfo?.artwork?.hdPic"></xImage>
</div>
<div class="px-[16px] bg-[#fff] pt-[11px] mb-6px">
<div class="text-[#000] text-[16px] mb-[12px]">{{detailInfo?.artworkTitle}}</div>
<div class="text-#575757 text-[14px] pb-8px">
<div class="flex mb-[4px]">
<div class="w-[70px]">作者</div>
<div>{{detailInfo?.artwork?.artistName??'-'}}</div>
</div>
<div class="flex mb-[4px]">
<div class="w-[70px] flex-shrink-0">总平尺数</div>
<div>{{detailInfo?.artwork?.ruler??'-'}}</div>
</div>
<div class="flex mb-[4px]">
<div class="w-[70px] flex-shrink-0">*</div>
<div>{{detailInfo?.artwork?.length}}*{{detailInfo?.artwork?.width}}cm</div>
</div>
<div class="flex mb-[4px]">
<div class="w-[70px] flex-shrink-0">画作简介</div>
<div>{{detailInfo?.artwork?.abstract??'-'}}</div>
</div>
</div>
</div>
<div class="flex px-[16px] bg-#fff h-[36px] items-center mb-6px">
<div class="text-[#575757] text-[14px]">起拍价</div>
<div class="text-#575757 text-14px font-bold">RMB 1,000</div>
</div>
<div class="px-[16px] bg-#fff pt-12px pb-18px">
<div class="text-[#575757] text-[14px] mb-4px">竞价表</div>
<div v-if="detailInfo?.priceRuleType!=='diy'">
<xImage :src="detailInfo?.priceRuleImage" alt=""/>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,183 @@
<template>
<div ref="dragRef"
:style="style"
class="fixed rounded-5px overflow-hidden shadow-lg cursor-move z-50"
@mousedown.stop="handleDragStart"
@touchstart.stop="handleDragStart">
<div class="relative" @click.stop="handleClick">
<img :src="props.snapshot"
class="w-80px object-cover"
alt="直播画面">
<div class="absolute inset-0 bg-black/40 flex items-center justify-center"
>
<span class="text-white text-12px">点击回到直播</span>
</div>
<button @click.stop="handleClose"
class="absolute top-10px right-10px text-white bg-black/40 rounded-full w-5 h-5 flex items-center justify-center hover:bg-black/60 cursor-pointer">
<van-icon name="cross" />
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onBeforeUnmount, shallowRef, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useThrottleFn } from '@vueuse/core'
const props = defineProps({
snapshot: {
type: String,
required: true
},
onClose: {
type: Function,
default: () => {}
},
onClick: {
type: Function,
default: () => {}
},
initialPosition: {
type: Object,
default: () => ({
top: '80px',
right: '16px'
})
}
})
const router = useRouter()
const dragRef = shallowRef(null)
const route = useRoute()
const isDragging = ref(false)
const startX = ref(0)
const startY = ref(0)
const left = ref(0)
const top = ref(0)
onMounted(() => {
const rect = dragRef.value.getBoundingClientRect()
left.value = window.innerWidth - rect.width - parseInt(props.initialPosition.right)
top.value = parseInt(props.initialPosition.top)
})
const style = computed(() => ({
left: left.value ? `${left.value}px` : 'auto',
top: top.value ? `${top.value}px` : 'auto',
right: !left.value ? props.initialPosition.right : 'auto',
transition: isDragging.value ? 'none' : 'all 0.3s ease',
}))
const handleDragStart = (event) => {
event.stopPropagation()
isDragging.value = true
const point = event.touches ? event.touches[0] : event
const rect = dragRef.value.getBoundingClientRect()
left.value = rect.left
top.value = rect.top
startX.value = point.clientX - left.value
startY.value = point.clientY - top.value
if (event.type === 'mousedown') {
document.addEventListener('mousemove', handleDragMove)
document.addEventListener('mouseup', handleDragEnd)
} else {
document.addEventListener('touchmove', handleDragMove, { passive: false })
document.addEventListener('touchend', handleDragEnd)
}
}
const handleDragMove = useThrottleFn((event) => {
if (!isDragging.value) return
event.preventDefault()
event.stopPropagation()
const point = event.touches ? event.touches[0] : event
const rect = dragRef.value.getBoundingClientRect()
const maxX = window.innerWidth - rect.width
const maxY = window.innerHeight - rect.height
left.value = Math.min(Math.max(0, point.clientX - startX.value), maxX)
top.value = Math.min(Math.max(0, point.clientY - startY.value), maxY)
}, 16)
const handleEdgeSnap = useThrottleFn(() => {
const rect = dragRef.value.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
left.value = centerX > window.innerWidth / 2
? window.innerWidth - rect.width - 16
: 16
}, 100)
const handleDragEnd = () => {
if (!isDragging.value) return
isDragging.value = false
handleEdgeSnap()
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
document.removeEventListener('touchmove', handleDragMove)
document.removeEventListener('touchend', handleDragEnd)
}
const handleClick = (event) => {
event.stopPropagation()
if (!isDragging.value) {
handleReturnLive()
}
}
const handleReturnLive = () => {
props.onClick()
}
const handleClose = (event) => {
event?.stopPropagation()
props.onClose()
}
watch(() => route.path, (newVal) => {
if (['/','/home'].includes(newVal)){
handleClose()
}
})
onBeforeUnmount(() => {
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
document.removeEventListener('touchmove', handleDragMove)
document.removeEventListener('touchend', handleDragEnd)
})
</script>
<style scoped>
.fixed {
will-change: transform, opacity;
touch-action: none;
-webkit-user-select: none;
user-select: none;
transform: translateZ(0);
}
.min-window-enter-active,
.min-window-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.min-window-enter-from,
.min-window-leave-to {
transform: translateY(100%);
opacity: 0;
}
img {
pointer-events: none;
-webkit-user-drag: none;
user-drag: none;
}
</style>

View File

@ -0,0 +1,28 @@
<script setup>
import { ref } from "vue";
const isButtonActive = ref(false);
const handleButtonPress = (event) => {
event.stopPropagation();
isButtonActive.value = true;
};
const handleButtonRelease = (event) => {
event.stopPropagation();
isButtonActive.value = false;
};
</script>
<template>
<div
:class="[
'transition-all duration-200',
isButtonActive ? 'scale-95' : ''
]"
@touchstart.stop="handleButtonPress"
@touchend.stop="handleButtonRelease"
>
<slot></slot>
</div>
</template>

View File

@ -0,0 +1,52 @@
<script setup>
import { showImagePreview } from 'vant';
const props = defineProps({
src: {
type: String,
default: ''
},
preview: {
type: Boolean,
default: true
},
//
sizes: {
type: Array,
default: () => [320, 640, 768, 1024]
},
//
format: {
type: String,
default: 'webp'
},
//
quality: {
type: Number,
default: 80
}
})
const showImage = () => {
if (props.preview) {
showImagePreview([props.src]);
}
}
</script>
<template>
<nuxt-img
loading="lazy"
v-bind="{ ...props, ...$attrs }"
style="object-fit: cover"
@click="showImage"
:src="src"
/>
</template>
<style scoped>
:deep(img) {
width: 100%;
height: 100%;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,93 @@
<script setup>
import { ref, computed } from 'vue'
import MessageContent from './message/index.vue'
const visible = ref(false)
const messageType = ref('success')
const messageText = ref('')
const showIcon = ref(true)
const customStyle = ref({})
const title = ref({})
const subTitle = ref({})
const containerStyle = computed(() => {
const { top, bottom, left, right, transform, ...otherStyles } = customStyle.value || {}
const baseStyle = {
position: 'fixed',
zIndex: 9999
}
const horizontalPosition = left || right
? { left, right }
: { left: '50%', transform: 'translateX(-50%)' }
const verticalPosition = {}
if (bottom !== undefined) {
verticalPosition.bottom = bottom
} else {
verticalPosition.top = top || '50px'
}
return {
...baseStyle,
...horizontalPosition,
...verticalPosition,
...otherStyles
}
})
const emit = defineEmits(['after-leave'])
const showMessage = (options) => {
if (typeof options === 'string') {
messageText.value = options
title.value = {}
subTitle.value = {}
} else {
messageText.value = options.message || ''
title.value = options.title || {}
subTitle.value = options.subTitle || {}
}
messageType.value = options.type || 'success'
showIcon.value = options.icon !== false
customStyle.value = options.style || {}
visible.value = true
setTimeout(() => {
visible.value = false
}, options.duration || 2000)
}
defineExpose({ showMessage })
</script>
<template>
<transition
name="fade"
@after-leave="$emit('after-leave')"
>
<MessageContent
v-if="visible"
:message="messageText"
:type="messageType"
:title="title"
:sub-title="subTitle"
:show-icon="showIcon"
:style="containerStyle"
/>
</transition>
</template>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,121 @@
<script setup>
import error from '../images/error.png'
import success from '../images/success.png'
import warning from '../images/warning.png'
import info from '../images/info.png'
const props = defineProps({
type: {
type: String,
default: 'success'
},
message: {
type: String,
default: ''
},
title: {
type: Object,
default: () => ({
text: '',
color: '',
align: 'left'
})
},
subTitle: {
type: Object,
default: () => ({
text: '',
color: '',
align: 'left'
})
},
showIcon: {
type: Boolean,
default: true
},
style: {
type: Object,
default: () => ({})
}
})
const typeConfig = {
info: {
imgSrc: info,
borderColor: '#C6DFFB',
bgColor: '#ECF5FE',
},
success: {
imgSrc: success,
borderColor: '#C5E7D5',
bgColor: '#EDF7F2',
},
error: {
imgSrc: error,
borderColor: '#FFD4D4',
bgColor: '#FFF0F0',
},
warning: {
imgSrc: warning,
borderColor: '#FFE2BA',
bgColor: '#FFF7EC',
}
}
// 使 props.style
const finalStyle = computed(() => {
return {
borderColor: props.style?.borderColor || typeConfig[props.type].borderColor,
backgroundColor: props.style?.backgroundColor || typeConfig[props.type].bgColor,
width: props.style?.width || '343px',
height: props.style?.height || 'auto',
minHeight: '46px',
...props.style
}
})
</script>
<template>
<div
:class="`box-border flex items-center border rounded-[4px] px-[15px] shadow-sm py-8px ${!message?'justify-center':''}`"
:style="finalStyle"
>
<div v-if="showIcon" class="mr-[12px]">
<img
:src="typeConfig[type].imgSrc"
class="w-20px h-20px"
style="object-fit: contain"
alt=""
>
</div>
<div class="flex flex-col justify-center">
<!-- 如果是简单文本模式 -->
<div v-if="message" class="text-[14px] line-height-none">
{{ message }}
</div>
<!-- 如果是标题+副标题模式 -->
<template v-else>
<div
v-if="title.text"
class="text-[14px] line-height-20px"
:style="{
color: title.color || 'inherit',
textAlign: title.align || 'left'
}"
>
{{ title.text }}
</div>
<div
v-if="subTitle.text"
class="text-[12px] leading-normal mt-1 line-height-17px"
:style="{
color: subTitle.color || '#939393',
textAlign: subTitle.align || 'left'
}"
>
{{ subTitle.text }}
</div>
</template>
</div>
</div>
</template>

View File

@ -0,0 +1,78 @@
import { createApp, nextTick } from 'vue'
import MessagePopup from './index.vue'
const message = {
success(options, duration = 2000) {
if (process.client) {
if (typeof options === 'string') {
this.show({ type: 'success', message: options, duration })
} else {
this.show({
type: 'success',
...options,
duration
})
}
}
},
error(options, duration = 2000) {
if (process.client) {
if (typeof options === 'string') {
this.show({ type: 'error', message: options, duration })
} else {
this.show({
type: 'error',
...options,
duration
})
}
}
},
info(options, duration = 2000) {
if (process.client) {
if (typeof options === 'string') {
this.show({ type: 'info', message: options, duration })
} else {
this.show({
type: 'error',
...options,
duration
})
}
}
},
warning(options, duration = 2000) {
if (process.client) {
if (typeof options === 'string') {
this.show({ type: 'warning', message: options, duration })
} else {
this.show({
type: 'warning',
...options,
duration
})
}
}
},
show(options) {
if (!process.client) return
const container = document.createElement('div')
document.body.appendChild(container)
const app = createApp(MessagePopup, {
onAfterLeave: () => {
app.unmount()
document.body.removeChild(container)
}
})
const instance = app.mount(container)
nextTick(() => {
instance.showMessage?.(options)
})
}
}
export { message }

View File

@ -0,0 +1,57 @@
<script setup>
/*
* 封装一个带标题栏的弹窗
* */
const props = defineProps({
show: {
type: Boolean,
default: false
},
title:''
})
const emit = defineEmits(['update:show'])
const close=()=>{
emit('update:show',false)
}
</script>
<template>
<van-popup
:show="show"
:transition-appear="true"
teleport="#__nuxt"
position="bottom"
@click-overlay="close"
:style="{ height: '74%' }"
v-bind="{...$attrs,...$props}"
:safe-area-inset-bottom="true"
>
<div class="flex flex-col h-full">
<!-- 标题栏 -->
<div class="flex items-center pl-16px pr-19px h-40px border-b-1px border-gray-300 shrink-0 relative w-full">
<slot v-if="$slots.title" name="title">
</slot>
<div v-else class="text-black text-16px text-center flex-grow-1">{{ title }}</div>
<van-icon
style="position: absolute"
class="right-19px"
size="20"
name="cross"
color="#939393"
@click="close"
/>
</div>
<!-- 内容区域 -->
<div class="flex-1 px-16px py-18px overflow-hidden relative">
<div class="h-full overflow-y-auto relative list-container">
<slot/>
</div>
</div>
</div>
</van-popup>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,109 @@
<script setup>
import { ref, computed } from 'vue'
import dayjs from 'dayjs'
const props = defineProps({
modelValue: {
type: [Date, String, Number],
default: () => new Date() //
},
label: {
type: String,
default: '日期'
},
required: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择日期'
},
disabled: {
type: Boolean,
default: false
},
minDate: {
type: Date,
default: () => new Date(1900, 0, 1)
},
maxDate: {
type: Date,
default: () => new Date(2100, 11, 31)
},
format: {
type: String,
default: 'YYYY-MM-DD'
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const show = ref(false)
//
const displayText = computed(() => {
return dayjs(props.modelValue).format(props.format)
})
//
const defaultValue = computed(() => {
const date = props.modelValue || new Date()
return [
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
]
})
//
const onConfirm = ({ selectedValues }) => {
show.value = false
const date = new Date(selectedValues[0], selectedValues[1] - 1, selectedValues[2])
emit('update:modelValue', date)
emit('change', date)
}
//
const onCancel = () => {
show.value = false
}
//
const reset = () => {
emit('update:modelValue', new Date())
}
defineExpose({
reset
})
</script>
<template>
<div>
<van-field
:model-value="displayText"
@click="show = true"
readonly
:disabled="disabled"
:required="required"
:placeholder="placeholder"
:label="label"
class="mb-10px"
is-link
/>
<van-popup
v-model:show="show"
position="bottom"
>
<van-date-picker
:min-date="minDate"
:max-date="maxDate"
:model-value="defaultValue"
@confirm="onConfirm"
@cancel="onCancel"
title="选择日期"
/>
</van-popup>
</div>
</template>

View File

@ -0,0 +1,88 @@
<script setup>
import { ref } from 'vue'
const props = defineProps({
value: {
type: [Number, String]
},
columns: {
type: Array,
default: () => []
},
label: {
type: String,
default: ''
},
required: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择'
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:value', 'change'])
const show = ref(false)
const onConfirm = (value) => {
show.value = false
emit('update:value', value.value)
emit('change', value)
}
const displayText = computed(() => {
const selected = props.columns.find(x => x.value === props.value)
return selected?.text || ''
})
const reset = () => {
emit('update:value', undefined)
}
defineExpose({
reset
})
</script>
<template>
<div>
<van-field
:model-value="displayText"
@click="show = true"
readonly
:disabled="disabled"
:required="required"
:placeholder="placeholder"
:label="label"
class="mb-10px"
is-link
/>
<van-popup
v-model:show="show"
destroy-on-close
position="bottom"
safe-area-inset-bottom
>
<van-picker
:columns="columns"
@confirm="onConfirm"
@cancel="show = false"
:default-index="columns.findIndex(x => x.value === value)"
title="请选择"
confirm-button-text="确定"
cancel-button-text="取消"
/>
</van-popup>
</div>
</template>

View File

@ -1,18 +0,0 @@
import { defineStore } from 'pinia'
const useCounter = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment,
}
}, {
persist: true,
})
export default useCounter

View File

@ -1,24 +0,0 @@
import type { RouteLocationNormalized, RouteRecordName } from 'vue-router'
import { defineStore } from 'pinia'
const useKeepalive = defineStore('keepalive', () => {
const routeCaches = ref<RouteRecordName[]>([])
const addRoute = (route: RouteLocationNormalized) => {
if (!route.name)
return
if (routeCaches.value.includes(route.name))
return
if (route?.meta?.keepalive)
routeCaches.value.push(route.name)
}
return {
routeCaches,
addRoute,
}
})
export default useKeepalive

View File

@ -0,0 +1,21 @@
export function useWebSocket() {
const { $ws } = useNuxtApp()
const messages = ref<any[]>([])
// 监听消息
const onMessage = (callback: (data: any) => void) => {
const handler = (event: CustomEvent) => callback(event.detail)
window.addEventListener('ws-message', handler as EventListener)
// 返回清理函数
return () => {
window.removeEventListener('ws-message', handler as EventListener)
}
}
return {
ws: $ws,
messages,
onMessage
}
}

3
app/config/index.js Normal file
View File

@ -0,0 +1,3 @@
export const useAppFooterRouteNames= ['index', 'profile']
export const useAppHeaderRouteNames= ['index', 'profile','login','collectCode-login','collectCode-mine']

View File

@ -1,6 +0,0 @@
import type { RouteRecordName } from 'vue-router'
/**
* Use the AppFooter routing whitelist
*/
export const useAppFooterRouteNames: RouteRecordName[] = ['index', 'profile']

0
app/config/live/index.js Normal file
View File

2
app/constants/index.js Normal file
View File

@ -0,0 +1,2 @@
export const appName = '豐和'
export const appDescription = '泰丰国际京都拍卖会'

View File

@ -1,2 +0,0 @@
export const appName = 'nuxt-vant-mobile'
export const appDescription = 'Nuxt H5 Starter Template'

View File

@ -7,7 +7,7 @@ By default, `default.vue` will be used unless an alternative is specified in the
```vue
<script setup lang="ts">
definePageMeta({
layout: 'home',
layout: 'goods',
})
</script>
```

View File

@ -1,11 +1,11 @@
<template>
<main class="flex flex-col min-h-svh">
<AppHeader class="h-[var(--van-nav-bar-height)]" />
<div class="flex-1 p-16 pb-[var(--van-nav-bar-height)]">
<div class="flex-1 flex flex-col">
<slot />
</div>
<AppFooter />
<AppFooter />
</main>
</template>
<script setup >
</script>

View File

@ -1,7 +0,0 @@
import type { RouteLocationNormalized } from 'vue-router'
import useKeepalive from '~/composables/keepalive'
export default defineNuxtRouteMiddleware((to: RouteLocationNormalized) => {
if (to.meta && to.meta.keepalive)
useKeepalive().addRoute(to)
})

View File

@ -0,0 +1,101 @@
<script setup>
import itemDetail from '@/components/itemDetail/index.vue'
import {userArtwork} from "~/api/goods/index.js";
const route = useRoute()
const detail = ref({})
const uuid = route.query.uuid
const initData = async () => {
const res = await userArtwork({uuid})
if (res.status === 0) {
detail.value = res.data
}
}
const position = ref({x: window?.innerWidth - 120 || 0, y: 240}) //
const startPosition = ref({x: 0, y: 0})
const isDragging = ref(false)
const startDrag = (e) => {
isDragging.value = true
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const clientY = e.touches ? e.touches[0].clientY : e.clientY
startPosition.value = {
x: clientX - position.value.x,
y: clientY - position.value.y
}
}
const onDrag = (e) => {
if (isDragging.value) {
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const clientY = e.touches ? e.touches[0].clientY : e.clientY
//
const maxX = window.innerWidth - 108 //
const maxY = window.innerHeight - 137 //
//
const x = Math.min(Math.max(0, clientX - startPosition.value.x), maxX)
const y = Math.min(Math.max(0, clientY - startPosition.value.y), maxY)
position.value = {x, y}
}
}
const stopDrag = () => {
isDragging.value = false
}
onMounted(() => {
//
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
//
document.addEventListener('touchmove', onDrag)
document.addEventListener('touchend', stopDrag)
})
onUnmounted(() => {
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('touchmove', onDrag)
document.removeEventListener('touchend', stopDrag)
})
initData()
</script>
<template>
<div class="relative h-screen-nav flex flex-col">
<itemDetail class="grow-1" :detail-info="detail.auctionArtworkInfo"/>
<div v-if="[1,3,4].includes(detail.status)" class="h-81px bg-#fff flex justify-center pt-7px">
<van-button class="w-213px !h-38px" type="primary">
<span class="text-#fff text-14px">去支付 RMB10,000</span>
</van-button>
</div>
<div
class="w-108px h-137px absolute cursor-move"
:style="{
left: position.x + 'px',
top: position.y + 'px'
}"
@mousedown="startDrag"
@touchstart.prevent="startDrag"
>
<img src="@/static/images/zd5530@2x.png" class="w-full h-full" alt="">
<div
class="flex flex-col items-center absolute bottom-25px text-14px text-#B58047 left-1/2 transform translate-x--1/2 whitespace-nowrap">
<div>恭喜您</div>
<div>竞拍成功</div>
</div>
</div>
</div>
</template>
<style scoped>
.cursor-move {
touch-action: none;
user-select: none;
}
</style>

View File

@ -0,0 +1,208 @@
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'
import { senCode, userLogin } from "@/api/auth/index.js";
import { codeAuthStore } from "@/stores-collect-code/auth/index.js";
import { message } from '@/components/x-message/useMessage.js'
// ... ...
import FingerprintJS from '@fingerprintjs/fingerprintjs'
import {checkPhone, mobileLogin, userSend} from "@/api-collect-code/auth/index.js";
const { userInfo, token,fingerprint } = codeAuthStore()
const router = useRouter();
const route = useRoute();
const { locale } = useI18n()
const loadingRef = ref({
loading1: false,
loading2: false,
})
const password = ref('')
const loginType = ref(0)
const interval = ref(null)
const startCountdown = () => {
if (interval.value) {
clearInterval(interval.value);
}
countdown.value = 60;
interval.value = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(interval.value);
}
}, 1000);
}
const countdown = ref(0);
const phoneNum = ref('17630920520')
const code = ref('123789')
const pane = ref(0)
const showKeyboard = ref(false);
const getFingerprint = async () => {
const fp = await FingerprintJS.load()
const result = await fp.get()
return result.visitorId //
}
//
const checkFingerprint = async () => {
const tempFingerprint = await getFingerprint()
if (fingerprint && fingerprint === tempFingerprint) {
await router.push('/collectCode/mine')
}
}
checkFingerprint()
const vanSwipeRef = ref(null)
const getCode = async () => {
loadingRef.value.loading1 = true
const res = await checkPhone({
tel: phoneNum.value,
})
loadingRef.value.loading1 = false
if (res.status === 0){
const res=await userSend({telNum:phoneNum.value,zone:'+86'})
if (res.status === 0){
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value = true
}
}
/* loadingRef.value.loading1 = false
if (res.status === 0) {
}
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value = true
startCountdown();*/
/* pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value=true
startCountdown();*/
}
const changeToPwd = async () => {
loginType.value = loginType.value === 0 ? 1 : 0
}
const goBack = () => {
code.value = ''
pane.value = 0
vanSwipeRef.value?.swipeTo(pane.value)
}
const goLogin = async () => {
loadingRef.value.loading2 = true
const res = await mobileLogin({
TelNum: phoneNum.value,
Password:loginType.value===1?password.value:'',
Code: loginType.value===0?code.value:''
})
if (res.status === 0) {
userInfo.value = res.data.accountInfo
token.value = res.data.token
fingerprint.value = await getFingerprint()
await router.push('/collectCode/mine');
}
loadingRef.value.loading2 = false
}
</script>
<template>
<div class="h-[100vh] w-[100vw] bg-[url('@/static/images/asdfsdd.png')] bg-cover px-[31px] pt-[86px]">
<div class="w-full flex justify-center mb-[100px] flex-col items-center">
<img class="h-[105px] w-[189px]" src="@/static/images/ghfggff.png" alt="">
<img class="h-[29px] w-[108px]" src="@/static/images/qrcodetext.png" alt="">
</div>
<van-swipe ref="vanSwipeRef" :show-indicators="false" :touchable="false" :lazy-render="true" :loop="false">
<van-swipe-item>
<div v-show="pane === 0">
<div class="">
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="phoneNum" clearable placeholder="请输入手机号">
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
手机号
</div>
</template>
</van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]" v-show="loginType === 1">
<van-field v-model="password" clearable placeholder="请输入密码">
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
密码
</div>
</template>
</van-field>
</div>
<div class="flex justify-end mt-[10px]" @click="changeToPwd">
<div class="text-[14px] text-[#2B53AC]">
{{ loginType === 0 ? '密码登录' : '验证码登录' }}
</div>
</div>
<div />
</div>
<div class="mt-[55px]">
<div v-if="loginType === 0">
<van-button :loading="loadingRef.loading1" v-if="phoneNum" loading-text="获取验证码"
type="primary" block style="height: 48px" @click="getCode">获取验证码</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">获取验证码</van-button>
</div>
<div v-else>
<van-button type="primary" v-if="password" block :loading="loadingRef.loading2" loading-text="登录"
style="height: 48px;margin-top:10px" @click="goLogin">登录</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">登录</van-button>
</div>
</div>
</div>
</van-swipe-item>
<van-swipe-item>
<div v-show="pane === 1">
<div class="flex mb-[16px]">
<div class="text-[16px] text-[#BDBDBD] mr-[10px]">{{ $t('login.hasSendTo') }}</div>
<div class="text-[16px] text-[#000]">+86 {{ phoneNum }}</div>
</div>
<van-password-input :value="code" :gutter="10" :mask="false" focused @focus="showKeyboard = true" />
<div :class="`${countdown > 0 ? 'text-#BDBDBD' : 'text-#2B53AC'} text-14px`">
{{ $t('login.reSend') }}<span v-if="countdown > 0">({{ countdown }})</span>
</div>
<div class="mt-[17px]">
<van-button v-if="code.length === 6" type="primary" block :loading="loadingRef.loading2"
:loading-text="$t('login.login')" style="height: 48px" @click="goLogin">{{
$t('login.login')
}}</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">{{
$t('login.login')
}}</van-button>
</div>
<div class="mt-[17px]">
<van-button type="primary" @click="goBack" block style="height: 48px">{{ $t('login.back')
}}</van-button>
</div>
</div>
</van-swipe-item>
</van-swipe>
<van-number-keyboard v-model="code" :show="showKeyboard" @blur="showKeyboard = false" />
</div>
</template>
<style scoped lang="scss">
:deep(.van-cell.van-field) {
padding-left: 0;
}
:deep(.van-password-input) {
margin: 0;
}
:deep(.van-password-input__item) {
border: 1px solid #E5E5E5;
width: 41px;
height: 41px;
}
</style>

View File

@ -0,0 +1,72 @@
<script setup>
import XImage from "@/components/x-image/index.vue";
import {useRuntimeConfig} from "#app";
import QRCode from 'qrcode'
import { showImagePreview } from 'vant';
import {offlineQrcodeDelete} from "~/api-collect-code/goods/index.js";
const statusLabel=[
{label:'已付款',value:2,color:'#18A058'}, {label:'未付款',value:1,color:'#CF3050'}, {label:'已部分付款',value:4,color:'#F09F1F'}
]
const props = defineProps({
data: {
type: Object,
default: () => {
return {};
},
},
});
const itemLabel=(data)=>{
return statusLabel.find(x=>x.value===data.payStatus)
}
const config = useRuntimeConfig()
const getQRBase64 = async () => {
try {
return await QRCode.toDataURL(`${config.public.NUXT_PUBLIC_API_BASE}/collectCode/payment`, {
width: 200,
margin: 4,
errorCorrectionLevel: 'H'
})
} catch (err) {
console.error('生成二维码失败:', err)
return null
}
}
const openQrCode=async ()=>{
const base64=await getQRBase64()
showImagePreview([base64])
}
</script>
<template>
<div class="flex flex-col h-120px bg-#F7F7F7 rounded-4px px-13px">
<div class="flex h-40px border-b border-b-#F0F0F0 items-center justify-between px-8px">
<div class="text-14px text-#000">¥ {{data.paidPrice}}/{{data.price}}</div>
<div :class="`text-12px text-${itemLabel(data).color}`">{{itemLabel(data).label}}</div>
</div>
<div class="flex flex-grow-1 px-8px py-11px">
<div class="mr-8px">
<XImage class="w-57px h-56px rounded-4px" :src="data.hdPic"></XImage>
</div>
<div class="text-12px text-#1E1E1E">
<div>Lot{{ data.lotNo }}</div>
<div>创建人{{ data.userName }}</div>
<div>创建时间{{data.createdAt}}</div>
</div>
<div class="flex flex-col justify-end ml-auto ">
<div class="flex w-55px h-26px bg-#2B53AC rounded-4px justify-center items-center">
<div @click="openQrCode" class="text-12px text-#fff line-height-none mt-0.5px mr-5px">查看</div>
<div >
<img class="w-12px h-12px" src="@/static/images/icon-design-42@3x.png" alt="">
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,210 @@
<script setup>
import { userArtworks } from "@/api/goods/index.js";
import { codeAuthStore } from "@/stores-collect-code/auth/index.js";
import { showImagePreview } from 'vant';
import XImage from '@/components/x-image/index.vue'
import {useRouter} from "#vue-router";
import {goodStore} from "~/stores-collect-code/goods/index.js";
import {ref} from "vue";
import {offlineQrcodeCreate, offlineQrcodeDelete, offlineQrcodeList} from "~/api-collect-code/goods/index.js";
import codeCard from './components/codeCard/index.vue'
import {message} from "~/components/x-message/useMessage.js";
definePageMeta({
layout: 'default',
i18n: 'menu.profile',
})
const router = useRouter();
const localState = ref({
finished: false,
refreshing: false,
showDetail: false,
showHeight: ''
})
const { userInfo, } = codeAuthStore()
const {getOfflineQrcodeList,itemList, loading: storeLoading,pageRef}= goodStore()
const initData = async () => {
onRefresh()
}
const show=ref(false)
const close=()=>{
console.log('show',show.value)
show.value=false
}
const logOut=()=>{
localStorage.clear()
router.push('/collectCode/login')
}
const createForm=ref({
lotNo:'',
price:'',
})
const confirm=async ()=>{
if (!createForm.value.price){
message.warning('请输入金额')
return false
}else if (!createForm.value.lotNo){
message.warning('请输入Lot号')
return false
}
const res=await offlineQrcodeCreate({...createForm.value,price:String(createForm.value.price)})
if (res.status===0){
show.value=false
}
}
const onRefresh = async () => {
try {
localState.value.refreshing = true
localState.value.finished = false
const { finished } = await getOfflineQrcodeList(true)
localState.value.finished = finished
} finally {
localState.value.refreshing = false
}
}
const loadMore = async () => {
pageRef.value.page++
const { finished } = await getOfflineQrcodeList()
localState.value.finished = finished
}
const abnormal=ref(false)
const abnormalRow=ref({})
const inputLotNo=async (data)=>{
const res=await offlineQrcodeList({
lotNo:createForm.value.lotNo
})
if (res.status===0){
if (res.data.Data?.length>0){
abnormal.value=true
abnormalRow.value=res.data.Data?.[0]
}
}
}
const deleteData=async (qrUid)=>{
const res=await offlineQrcodeDelete({
qrUid:qrUid
})
if (res.status===0){
getOfflineQrcodeList()
message.success('删除成功')
}
}
initData()
</script>
<template>
<div class="w-[100vw] bg-[url('@/static/images/3532@2x.png')] h-screen-nav bg-cover pt-43px flex-grow-1 flex flex-col">
<div class="flex items-center px-16px mb-43px">
<div class="mr-23px">
<img class="w-57px h-57px" src="@/static/images/5514@2x.png" alt="">
</div>
<div class="flex flex-col">
<div class="text-18px text-#181818">{{ userInfo.realName }}</div>
<div class="text-#575757 text-14px">{{ userInfo.telNum }}</div>
</div>
<div class="grow-1 flex justify-end" @click="logOut">
<img class="w-40px h-40px" src="@/static/images/logout.png" alt="">
</div>
</div>
<div class="border-b-1px border-b-#D3D3D3 px-16px flex">
<div class="text-#000 text-16px border-b-3 border-b-#2B53AC h-36px">线下付款二维码 </div>
</div>
<div class="grow-1 flex flex-col overflow-hidden py-15px">
<div class="overflow-auto">
<van-pull-refresh v-model="localState.refreshing"
success-text="刷新成功"
:success-duration="700"
@refresh="onRefresh">
<van-list v-model:loading="storeLoading"
:finished="localState.finished"
finished-text="没有更多了"
@load="loadMore" class="px-14px">
<template v-for="(item,index) of itemList" :key="item.qrUid">
<template v-if="item.payStatus===1">
<van-swipe-cell class="mb-14px" >
<codeCard :data="item"></codeCard>
<template #right>
<div class="w-65px h-full bg-#CF3050 flex items-center justify-center" @click="deleteData(item.qrUid)">
<img class="w-22px h-24px" src="@/static/images/delete3@.png" alt="">
</div>
</template>
</van-swipe-cell>
</template>
<template v-else>
<div class="mb-14px">
<codeCard :data="item"></codeCard>
</div>
</template>
</template>
</van-list>
</van-pull-refresh>
</div>
</div>
<div class="h-81px w-full flex justify-center shrink-0 pt-10px">
<div class="w-213px h-38px bg-#2B53AC text-#fff flex justify-center items-center text-14px rounded-4px" @click="show=true">
新增
</div>
</div>
<van-dialog v-model:show="show">
<div class="pt-18px pb-24px px-24px">
<div class="text-16px text-#000 font-bold text-center mb-26px">新增收款二维码</div>
<div class="">
<div class="flex mb-6px items-center">
<div class="w-58px">
<div class="text-#1A1A1A text-16px">金额</div>
<div class="text-#939393 text-12px">RMB</div>
</div>
<div>
<input v-model="createForm.price" type="number"
class="w-214px h-48px bg-#F3F3F3 rounded-4px px-11px text-16px" placeholder="请输入金额">
</div>
</div>
<div class="flex items-center">
<div class="w-58px">
<div class="text-#1A1A1A text-16px">Lot号</div>
</div>
<div>
<input type="number" v-model="createForm.lotNo" @input="inputLotNo" class="w-214px h-48px bg-#F3F3F3 rounded-4px px-11px text-16px" placeholder="请输入拍品序号">
</div>
</div>
</div>
<div class="flex flex-col items-center" v-if="abnormal">
<div class="text-#CF3050 text-12px mb-8px mt-4px">*该拍品号当前已存在收款二维码确定要创建吗</div>
<div>
<XImage class="w-116px h-116px rounded-4px mb-9px" :src="abnormalRow.hdPic"></XImage>
<div class="text-12px text-#575757 flex flex-col items-center">
<div>日出而作日落而息</div>
<div>张天赐</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="border-t flex">
<van-button class="w-50% h-56px" style="border: none;border-radius: 0;border-right: 1.5px solid #E7E7E7" @click="show=false">
<span class="text-#000 text-16px text-center">取消</span>
</van-button>
<van-button class="w-50% h-56px !rounded-0" style="border: none;border-radius: 0" @click="confirm">
<span class="text-#000 text-16px text-center text-#2B53AC">确定</span>
</van-button>
</div>
</template>
</van-dialog>
</div>
</template>
<style scoped lang="scss">
:deep(.van-hairline--top.van-dialog__footer){
&>.van-button{
border-top: 1px solid #E7E7E7;
&.van-dialog__cancel{
border-right: 1px solid #E7E7E7;
}
}
}
</style>

View File

@ -0,0 +1,43 @@
<script setup>
const payStatus=ref(0)
const changePayStatus=()=>{
payStatus.value=payStatus.value===0?1:0
}
const validateInput = (e) => {
const value = e.target.value
const char = String.fromCharCode(e.charCode)
if (!/[\d.]/.test(char)) {
e.preventDefault()
return
}
if (char === '.' && (value.includes('.') || !value)) {
e.preventDefault()
return
}
if (value.includes('.') && value.split('.')[1]?.length >= 2) {
e.preventDefault()
return
}
}
</script>
<template>
<div class="w-[100vw] h-screen-nav bg-[url('@/static/images/3532@2x.png')] bg-cover flex-grow-1 flex flex-col items-center pt-183px">
<div class="mb-30px">
<img class="w-126px h-126px" src="@/static/images/dddf34@2x.png" alt="">
</div>
<div class="text-#1A1A1A text-16px mb-25px font-bold">{{payStatus===0?'支付全部':'支付部分'}}</div>
<div class="text-#999999 text-16px mb-24px font-bold" v-if="payStatus===0">RMB 5000</div>
<div class="mb-12px">
<input class="w-272px h-48px bg-#F3F3F3 px-11px text-16px" type="text" placeholder="最多RMB5,000" @keydown="validateInput">
</div>
<div class="text-#2B53AC text-14px" @click="changePayStatus">{{payStatus===1?'支付全部':'支付部分'}}</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,18 @@
<script setup>
const image = ref('');
import { showToast } from 'vant';
const onSubmit = (data) => {
image.value = data.image;
};
const onClear = () => showToast('clear');
</script>
<template>
<van-signature @submit="onSubmit" @clear="onClear" />
<van-image v-if="image" :src="image" />
</template>
<style scoped>
</style>

View File

@ -0,0 +1,58 @@
<script setup>
import {useI18n} from "vue-i18n";
import XVanSelect from '@/components/x-van-select/index.vue'
import XVanDate from '@/components/x-van-date/index.vue'
definePageMeta({
layout: 'default',
i18n: 'menu.profile',
})
const {t} = useI18n()
const showPicker = ref(false)
const showPicker1 = ref(false)
const onConfirm = () => {
}
const columns = ref([
{text: t('realAuth.male'), value: 1},
{text: t('realAuth.female'), value: 2},
])
</script>
<template>
<div
class="w-[100vw] bg-[url('@/static/images/asdfsdd.png')] h-screen-nav bg-cover pt-77px flex-grow-1 flex flex-col ">
<div class="text-16px text-#191919 font-bold mb-40px px-34px">
请填写个人相关信息
</div>
<div class="grow-1 px-34px">
<van-field type="tel" :label-width="161" label="文本" class="mb-10px" placeholder="请输入手机号">
<template #label>
<div class="flex">
<div class="mr-41px whitespace-nowrap">手机号</div>
<div>
<span class="mr-13px">+ 86</span>
<van-icon name="arrow-down" class="text-#777777"/>
</div>
</div>
</template>
</van-field>
<van-field label="姓名" class="mb-10px" placeholder="请输入姓名"/>
<x-van-select label="性别" :columns="columns"/>
<x-van-date label="出生日期"/>
<van-field label="家庭住址" class="mb-10px" placeholder="请输入家庭住址"/>
<van-field label="所属银行" class="mb-10px" placeholder="请输入所属银行"/>
<van-field label="银行卡号码" class="mb-10px" placeholder="请输入银行卡号码"/>
</div>
<div class="h-81px bg-#fff flex justify-center pt-7px border-t">
<van-button color="#2B53AC" class="w-213px van-btn-h-38px">下一步</van-button>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.van-cell.van-field){
padding-left: 0;
}
</style>

View File

@ -0,0 +1,40 @@
<script setup>
const activeNames = ref(['1']);
</script>
<template>
<div class="bg-#EBEBEB h-screen-nav flex flex-col">
<div class="h-50px text-14px text-#191919 bg-#fff flex items-center px-21px mb-6px">支付前需同意以下内容并签字</div>
<van-collapse v-model="activeNames" class="grow-1">
<van-collapse-item name="1" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
<van-collapse-item name="2" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
<van-collapse-item name="3" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
</van-collapse>
<div class="h-81px bg-#fff flex justify-center pt-7px border-t">
<van-button color="#2B53AC" class="w-213px van-btn-h-38px">同意并签字</van-button>
</div>
</div>
</template>
<style scoped>
:deep(.van-cell__right-icon){
color: #ACACAC;
font-size: 12px;
}
</style>

View File

@ -1,35 +0,0 @@
<script setup lang="ts">
import useCounter from '~/composables/counter'
definePageMeta({
title: '🍍 持久化 Pinia 状态',
i18n: 'menu.persistPiniaState',
})
const counter = useCounter()
function add() {
counter.increment()
}
</script>
<template>
<div>
<h1 class="text-6xl color-pink font-semibold">
Hello, Pinia!
</h1>
<p class="mt-10 text-gray-700 dark:text-white">
{{ $t('counter_page.label') }}
</p>
<p class="mt-10">
{{ $t('counter_page.label_num') }}:
<strong class="text-green-500"> {{ counter.count }} </strong>
</p>
<button class="mt-10 btn" @click="add">
{{ $t('counter_page.btn_add') }}
</button>
</div>
</template>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,186 @@
<script setup>
import {ref, computed, watch} from 'vue';
import pinyin from 'pinyin';
import countryCode from './data/index.js';
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
definePageMeta({
title: '国家地区',
i18n: 'countryRegion.title',
})
const router = useRouter()
console.log('router',router)
const { t, locale } = useI18n()
const value = ref('');
const alphabet = [
'#',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];
//
const frequentCountryCodes = ['CN', 'TW', 'JP', 'US'];
function groupByPinyinInitial(data) {
const grouped = {};
//
grouped['#'] = [];
data.forEach(country => {
if (frequentCountryCodes.includes(country.code)) {
const countryName = locale.value === 'zh-CN' ? country.cn :
locale.value === 'zh-TW' ? country.tw :
locale.value === 'ja-JP' ? country.ja :
country.en;
grouped['#'].push({
...country,
displayName: countryName
});
}
});
//
data.forEach(country => {
if (!frequentCountryCodes.includes(country.code)) {
const countryName = locale.value === 'zh-CN' ? country.cn :
locale.value === 'zh-TW' ? country.tw :
locale.value === 'ja-JP' ? country.ja :
country.en;
const initial = locale.value === 'ja-JP' ? '' :
locale.value === 'zh-CN' || locale.value === 'zh-TW' ?
pinyin(countryName, {style: pinyin.STYLE_FIRST_LETTER})[0][0].toUpperCase() :
countryName.charAt(0).toUpperCase();
if (!grouped[initial]) {
grouped[initial] = [];
}
grouped[initial].push({
...country,
displayName: countryName
});
}
});
if (locale.value === 'ja-JP') {
//
grouped[''] = grouped[''].sort((a, b) => a.displayName.localeCompare(b.displayName, 'ja-JP'));
}
return grouped;
}
const groupedCountries = ref([])
const initData = () => {
groupedCountries.value = groupByPinyinInitial(countryCode);
}
const searchCountry = computed(() => {
if (!value.value) {
return groupedCountries.value;
}
return Object.keys(groupedCountries.value).reduce((filtered, initial) => {
const countries = groupedCountries.value[initial].filter(country =>
country.displayName.toLowerCase().includes(value.value.toLowerCase())
);
if (countries.length > 0) {
filtered[initial] = countries;
}
return filtered;
}, {});
});
const showIndexBar = computed(() => locale.value !== 'ja-JP')
const route = useRoute()
const handleCountrySelect = (country) => {
router.replace({
path: window.history.state.back,
query: {
zone: country.zone,
countryName: country.displayName
}
})
}
initData()
//
watch(locale, () => {
initData()
})
</script>
<template>
<div>
<van-sticky>
<van-search v-model="value" :placeholder="t('countryRegion.searchPlaceholder')"/>
</van-sticky>
<van-index-bar
v-if="showIndexBar"
sticky
:sticky-offset-top="55"
:index-list="alphabet"
>
<!-- 常用国家分类 -->
<van-index-anchor index="#">{{ t('countryRegion.frequentCountry') }}</van-index-anchor>
<van-cell
v-for="country in searchCountry['#']"
:key="country.code"
:title="country.displayName"
@click="handleCountrySelect(country)"
clickable
>
<div class="pr-[25px]"> +{{ country.zone }}</div>
</van-cell>
<!-- 其他国家按字母分类 -->
<template v-for="(countries, index) in searchCountry" :key="index">
<template v-if="index !== '#'">
<van-index-anchor
:index="index"
></van-index-anchor>
<van-cell
v-for="country in countries"
:key="country.code"
:title="country.displayName"
@click="handleCountrySelect(country)"
clickable
>
<div class="pr-[25px]"> +{{ country.zone }}</div>
</van-cell>
</template>
</template>
</van-index-bar>
<div v-else>
<div class="mb-4">
<div class="px-4 py-2 text-gray-600">{{ t('countryRegion.frequentCountry') }}</div>
<van-cell
v-for="country in searchCountry['#']"
:key="country.code"
:title="country.displayName"
@click="handleCountrySelect(country)"
clickable
>
<div class="pr-[25px]"> +{{ country.zone }}</div>
</van-cell>
</div>
<van-cell
v-for="country in Object.values(searchCountry).flat().filter(c => !frequentCountryCodes.includes(c.code))"
:key="country.code"
:title="country.displayName"
@click="handleCountrySelect(country)"
clickable
>
<div class="pr-[25px]"> +{{ country.zone }}</div>
</van-cell>
</div>
<van-back-top v-if="showIndexBar" right="15vw" bottom="10vh"/>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,19 @@
<script setup>
import {goodStore} from "@/stores/goods/index.js";
import xImage from '@/components/x-image/index.vue'
const {
auctionDetail
} = goodStore()
</script>
<template>
<div class="px-16px pt-14px">
<div class="text-#575757 text-14px" v-html="auctionDetail.info">
</div>
<xImage :src="auctionDetail.image" class="w-343px"></xImage>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,28 @@
<script setup>
import xPopup from '@/components/x-popup/index.vue'
import ItemDetail from "@/components/itemDetail/index.vue";
import {goodStore} from "@/stores/goods/index.js";
const {
artWorkDetail
} = goodStore()
const props = defineProps({
show: {
type: Boolean,
default: false
},
detailInfo: {
type: Object,
default: null
}
})
const emit = defineEmits(['update:show'])
const handleClose = () => {
emit('update:show', false)
}
</script>
<template>
<xPopup :show="show" title="拍品详情" @update:show="handleClose">
<ItemDetail :detailInfo="detailInfo" />
</xPopup>
</template>

View File

@ -0,0 +1,116 @@
<script setup>
import { ref, computed } from 'vue'
import { useRect } from "@vant/use"
import { goodStore } from "@/stores/goods"
import DetailPopup from '../DetailPopup/index.vue'
import MasonryWall from '@yeger/vue-masonry-wall'
const {
itemList,
pageRef,
auctionDetail,
liveRef,
artWorkDetail,
currentItem,
loading: storeLoading,
getArtworkList,
getArtworkDetail
} = goodStore()
const localState = ref({
finished: false,
refreshing: false,
showDetail: false,
showHeight: ''
})
//
const loadMore = async () => {
pageRef.value.page++
const { finished } = await getArtworkList()
localState.value.finished = finished
}
//
const onRefresh = async () => {
try {
localState.value.refreshing = true
localState.value.finished = false
const { finished } = await getArtworkList(true)
localState.value.finished = finished
} finally {
localState.value.refreshing = false
}
}
//
const openShow = async (item) => {
localState.value.showDetail = true
currentItem.value = item
getArtworkDetail(item.uuid)
}
</script>
<template>
<div class="px-[16px] pt-[16px]">
<van-pull-refresh
v-model="localState.refreshing"
success-text="刷新成功"
:success-duration="700"
@refresh="onRefresh"
>
<template #success>
<van-icon name="success" /> <span>刷新成功</span>
</template>
<van-list
v-model:loading="storeLoading"
:finished="localState.finished"
finished-text="没有更多了"
@load="loadMore"
>
<div class="w-full flex gap-[16px]">
<masonry-wall :items="itemList" :ssr-columns="2" :maxColumns="2" :minColumns="2" :gap="5">
<template #default="{ item, index }">
<div
@click="openShow(item)"
class="w-full"
>
<div class="relative w-full">
<img
:src="item.artwork?.hdPic"
class="w-full object-cover rounded-4px"
loading="lazy"
/>
<div
class="absolute rounded-2px overflow-hidden line-height-12px left-[8px] top-[8px] h-[17px] w-[45px] flex items-center justify-center bg-[#2b53ac] text-[12px] text-[#fff]"
>
LOT{{ item.index }}
</div>
</div>
<div class="pt-[8px]">
<div class="text-[14px] text-[#000000] leading-[20px]">
{{ item.name }}
</div>
<div class="mt-[4px] text-[12px] text-[#575757]">
起拍价{{ item?.startPrice??0 }}
</div>
<div
v-if="item.soldPrice"
class="mt-[4px] text-[12px] text-[#b58047]"
>
成交价{{ item?.startPrice??0 }}
</div>
</div>
</div>
</template>
</masonry-wall>
</div>
</van-list>
</van-pull-refresh>
<DetailPopup v-model:show="localState.showDetail" :detailInfo="artWorkDetail"></DetailPopup>
</div>
</template>
<style scoped>
.content {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
</style>

77
app/pages/home/index.vue Normal file
View File

@ -0,0 +1,77 @@
<script setup>
import liveRoom from '@/pages/liveRoom/index.client.vue';
import {goodStore} from "@/stores/goods/index.js";
import ItemList from './components/ItemList/index.vue'
import Cescribe from './components/Cescribe/index.vue'
import {message} from '@/components/x-message/useMessage.js'
import {liveStore} from "~/stores/live/index.js";
const {getAuctionDetail,auctionDetail} = goodStore();
const {fullLive}= liveStore()
const changeLive = () => {
fullLive.value = true;
};
if (!auctionDetail.value.uuid){
await getAuctionDetail()
}
</script>
<template>
<div class="flex-grow-1">
<client-only>
<liveRoom @click="changeLive" :class="['changeLive', fullLive ? 'expanded' : 'collapsed']"/>
</client-only>
<div v-if="!fullLive" class="bg-#fff">
<van-tabs sticky animated>
<van-tab title="拍品列表">
<ItemList></ItemList>
</van-tab>
<van-tab title="拍卖说明">
<Cescribe></Cescribe>
</van-tab>
</van-tabs>
<van-back-top right="15vw" bottom="10vh"/>
</div>
</div>
</template>
<style scoped lang="scss">
.ellipsis {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.van-swipe__indicator) {
width: 8px;
height: 8px;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 1s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
:deep(.van-swipe__indicator:not(.van-swipe__indicator--active)) {
background: rgba(0, 0, 0, 0.8);
}
.changeLive {
width: 100%;
overflow: hidden;
transition: height 0.4s ease, transform 0.4s ease;
}
.changeLive.collapsed {
height: 188px;
}
.changeLive.expanded {
position: absolute;
z-index: 10;
height: calc(100vh - var(--van-nav-bar-height));
}
</style>

View File

@ -1,95 +1,11 @@
<script setup lang="ts">
import type { LocaleObject } from '@nuxtjs/i18n'
import type { PickerColumn } from 'vant'
import type { ComputedRef } from 'vue'
import { Locale } from 'vant'
<script setup>
import Home from './home/index.vue'
definePageMeta({
layout: 'default',
title: '主页',
i18n: 'menu.home',
})
const color = useColorMode()
useHead({
meta: [{
id: 'theme-color',
name: 'theme-color',
content: () => color.value === 'dark' ? '#222222' : '#ffffff',
}],
})
const checked = computed({
get: () => color.value === 'dark',
set: (val: boolean) => {
color.preference = val ? 'dark' : 'light'
},
})
const { setLocale, t } = useI18n()
const i18n = useNuxtApp().$i18n
const showLanguagePicker = ref(false)
const languageValues = ref<string[]>([i18n.locale.value])
const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
const menus = computed(() => [
{ title: t('menu.unocssExample'), route: 'unocss' },
{ title: t('menu.keepAlive'), route: 'keepalive' },
{ title: t('menu.persistPiniaState'), route: 'counter' },
{ title: t('menu.fetch'), route: 'prose' },
{ title: t('menu.404Demo'), route: 'unknown' },
])
function onLanguageConfirm(event: { selectedOptions: PickerColumn }) {
const lang = event.selectedOptions[0]?.code
setLocale(lang)
Locale.use(lang)
localStorage.setItem('lang', lang)
showLanguagePicker.value = false
}
</script>
<template>
<div>
<VanCellGroup inset>
<VanCell :title="$t('menu.darkMode')" center>
<template #right-icon>
<ClientOnly>
<VanSwitch
v-model="checked"
size="20px"
aria-label="on/off Dark Mode"
/>
</ClientOnly>
</template>
</VanCell>
<VanCell
:title="$t('menu.language')"
:value="locales.find(i => i.code === i18n.locale.value)?.name"
is-link
@click="showLanguagePicker = true"
/>
<template v-for="item in menus" :key="item.route">
<VanCell :title="item.title" :to="item.route" is-link />
</template>
</VanCellGroup>
<van-popup v-model:show="showLanguagePicker" position="bottom">
<van-picker
v-model="languageValues"
:columns="locales"
:columns-field-names="{ text: 'name', value: 'code' }"
@confirm="onLanguageConfirm"
@cancel="showLanguagePicker = false"
/>
</van-popup>
</div>
<Home/>
</template>

View File

@ -1,21 +1,17 @@
<script setup lang="ts">
defineOptions({
name: 'Keepalive',
})
definePageMeta({
name: 'Keepalive',
keepalive: true,
title: '🧡 KeepAlive',
i18n: 'menu.keepAlive',
})
const value = ref(1)
</script>
<template>
<div>
<p> {{ $t('keepalive_page.label') }} </p>
<van-stepper v-model="value" class="mt-10" />
<div class="h-[100vh] w-[100vw]">
<SignaturePad v-model="signature" @change="handleSignatureChange"/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import SignaturePad from '@/components/SignaturePad.vue'
const signature = ref('')
const handleSignatureChange = (imageData) => {
// imageData base64
console.log('签名已更新:', imageData)
}
</script>

View File

@ -0,0 +1,62 @@
<script setup>
import {liveStore} from "@/stores/live/index.js";
import {authStore} from "~/stores/auth/index.js";
const {auctionData} = liveStore()
const {userInfo}= authStore()
const headList=[
{
label:'领先',
color:'#D03050',
value:'head'
},
{
label:'出局',
color:'#939393',
value:'out'
},
{
label:'成交',
color:'#34B633',
value:'success'
}
]
const headItem=(statusCode)=>{
return headList.find(x=>x.value===statusCode)
}
</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="auctionData.wsType==='stopArtwork'">
<div class="text-#939393 text-14px">即将开始下一个拍品</div>
</template>
<template v-else-if="auctionData.auctionPriceList?.buys?.length>0">
<div v-for="(item, index) in auctionData.auctionPriceList?.buys" :key="index" class="flex flex-shrink-0 h-25px">
<div class="text-start shrink-0 w-60px" :style="`color: ${headItem(item.statusCode).color}`" >{{ headItem(item.statusCode).label }}</div>
<div class="text-start shrink-0 w-80px">{{ item.auctionType==='local'?'现场竞价':'网络竞价' }}</div>
<div class="text-start shrink-0 w-80px">{{ item.createdAt }}</div>
<div class="text-start shrink-0 w-80px">{{item.baseCurrency}}{{ item.baseMoney }}</div>
<div class="text-start text-#2B53AC shrink-0 w-20px">{{ item.userId===userInfo.ID?'我':'' }}</div>
</div>
</template>
<template v-if="auctionData.wsType==='newArtwork'">
<div class="text-#939393 text-14px">开始拍卖</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,92 @@
<script setup>
import {liveStore} from "~/stores/live/index.js";
import { showMinWindow, hideMinWindow } from '@/components/liveMinWindow/createMinWindow.js'
const {lastSnapshot,fullLive} = liveStore()
const props = defineProps({
show: {
type: Boolean,
default: false
},
price: {
type: Number,
default: 0
}
})
const router = useRouter()
const emit = defineEmits(['update:show'])
const payStatus=ref(0)
const changePayStatus=()=>{
payStatus.value=payStatus.value===0?1:0
}
const close=()=>{
emit('update:show',false)
}
const confirm=()=>{
router.push('/signature/protocol')
handleCapture()
emit('update:show',false)
}
const captureVideoFrame = () => {
try {
const video = document.querySelector('#J_prismPlayer video')
if (!video) {
console.error('未找到视频元素')
return null
}
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
return canvas.toDataURL('image/jpeg', 0.9)
} catch (error) {
console.error('获取视频截图失败:', error)
return null
}
}
const handleCapture = () => {
const imageUrl = captureVideoFrame()
if (imageUrl) {
lastSnapshot.value=imageUrl
showMinWindow(lastSnapshot.value,{
onClick:()=>{
router.replace('/')
fullLive.value=true
console.log('执行')
}
})
}
}
</script>
<template>
<div>
<van-dialog :show="show" show-cancel-button @cancel="close" @confirm="confirm">
<div class="flex flex-col pt-18px pb-13px justify-between items-center h-144px">
<template v-if="payStatus===0">
<div class="text-#000 text-16px font-600 ">支付全部</div>
<div class="text-#000 text-16px ">RMB 5,000</div>
</template>
<template v-if="payStatus===1">
<div class="text-#000 text-16px font-600 ">支付部分</div>
<input class="w-272px h-48px bg-#F3F3F3 px-11px text-16px" type="text" placeholder="最多RMB5,000">
</template>
<div class="text-#2B53AC text-14px" @click="changePayStatus">{{payStatus===0 ? '支付部分' : '支付全部'}}</div>
</div>
</van-dialog>
</div>
</template>
<style scoped lang="scss">
:deep(.van-hairline--top.van-dialog__footer){
&>.van-button{
border-top: 1px solid #E7E7E7;
&.van-dialog__cancel{
border-right: 1px solid #E7E7E7;
}
}
}
</style>

View File

@ -0,0 +1,42 @@
<script setup>
import successImg from '@/static/images/zu5554@2x.png'
import errorImg from '@/static/images/zu5561@2x.png'
const props = defineProps({
show: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'success'
},
price: {
type: Number,
default: 1000
}
})
const emit = defineEmits(['cancel','update:show'])
const cancel= () => {
emit('update:show', false)
}
</script>
<template>
<div>
<van-dialog style="overflow: visible" :show="show" show-cancel-button :show-confirm-button="false" cancelButtonText="返回" cancelButtonColor="#2B53AC" @cancel="cancel">
<div class="h-145px relative flex justify-center">
<img :src="type==='success' ? successImg : errorImg" class="w-119px h-120px absolute top--74px z-9999 left-1/2 transform translate-x--1/2" alt="">
<div class="mt-94px text-#A9A9A9 text-16px">{{price}}</div>
</div>
</van-dialog>
</div>
</template>
<style scoped>
:deep(.van-hairline--top.van-dialog__footer){
border-top: 1px solid #E7E7E7;
border-bottom-left-radius:8px ;
border-bottom-right-radius:8px ;
}
</style>

View File

@ -0,0 +1,68 @@
<script setup>
import {ref} from "vue";
import lockClosed from "@/static/images/lockdfd@2x.png";
import lockOpen from "@/static/images/lock4@2x.png";
import {liveStore} from "@/stores/live/index.js";
import xButton from '@/components/x-button/index.vue'
import tangPopup from './tangPopup.vue'
import {goodStore} from "@/stores/goods/index.js";
import {authStore} from "~/stores/auth/index.js";
const {quoteStatus, changeStatus, show, auctionData, getSocketData} = liveStore();
const {pageRef} = goodStore();
const {userInfo} = authStore()
const showTang = ref(false)
const openOne = () => {
showTang.value = true
}
const paySide = computed(() => {
//
if (auctionData.value.artwork?.isSoled && auctionData.value.artwork?.buyInfo.userID === userInfo.value.ID) {
return true
} else {
return false
}
})
const goPay = () => {
show.value = true
}
</script>
<template>
<div class="bg-white w-60px rounded-l-4px overflow-hidden">
<!-- 拍品信息 -->
<van-button class="w-60px !h-60px" @click="openOne" style="border: none;border-radius: 0">
<div class="text-center flex flex-col justify-center items-center text-#7D7D7F text-12px">
<div>拍品</div>
<div>({{ auctionData?.artwork?.index }}/{{ pageRef.itemCount ?? 0 }})</div>
</div>
</van-button>
<tangPopup v-model:show="showTang"></tangPopup>
<!-- 出价开关 -->
<van-button class="w-60px !h-60px" @click="changeStatus"
style="border-right: none;border-left: none;border-radius: 0;padding: 0">
<div class="text-center flex flex-col justify-center items-center">
<div class="mb-4px">
<img
:src="quoteStatus ? lockClosed : lockOpen"
class="w-16px h-21px"
alt="锁图标"
/>
</div>
<div :class="quoteStatus ? 'text-gray-500' : 'text-blue-600'" class="text-10px transition-colors duration-200">
{{ quoteStatus ? '关闭出价' : '开启出价' }}
</div>
</div>
</van-button>
<!-- 支付 -->
<van-button v-if="paySide" class="w-60px !h-60px" style="border: none;border-radius: 0" @click="goPay">
<div class="text-center flex flex-col justify-center items-center text-yellow-600">
<div class="text-10px">RMB</div>
<div class="text-12px">5,000</div>
<div class="text-10px">去支付</div>
</div>
</van-button>
</div>
</template>

View File

@ -0,0 +1,150 @@
<script setup>
import xPopup from '@/components/x-popup/index.vue'
import {goodStore} from "@/stores/goods/index.js";
import xImage from '@/components/x-image/index.vue'
import DetailPopup from '@/pages/home/components/DetailPopup/index.vue'
import {liveStore} from "~/stores/live/index.js";
import {ref} from "vue";
const {pageRef,itemList,getArtworkList, loading: storeLoading,} = goodStore();
const {auctionData} = liveStore()
const showDetail=ref(false)
const localState = ref({
finished: false,
refreshing: false,
showDetail: false,
showHeight: ''
})
const onRefresh = async () => {
try {
localState.value.refreshing = true
localState.value.finished = false
const { finished } = await getArtworkList(true)
localState.value.finished = finished
} finally {
localState.value.refreshing = false
}
}
const props = defineProps({
show: Boolean,
title: {
type: String,
default: ''
}
})
const scrollToCurrentItem = () => {
if (!itemList.value?.length) return
const currentIndex = itemList.value.findIndex(
item => auctionData.value.artwork.index === item?.index
)
if (currentIndex > -1) {
const container = document.querySelector('.list-container')
const targetElement = document.querySelectorAll('.item-wrapper')[currentIndex]
if (targetElement && container) {
const containerTop = container.getBoundingClientRect().top
const elementTop = targetElement.getBoundingClientRect().top
const scrollTop = elementTop - containerTop + container.scrollTop
container.scrollTo({
top: scrollTop,
behavior: 'smooth'
})
}
}
}
const emit = defineEmits(['update:show'])
const showDetailInfo=ref(null)
const close = () => emit('update:show', false);
const openShow=(item)=>{
showDetailInfo.value=item
showDetail.value=true
}
const loadMore = async () => {
pageRef.value.page++
const { finished } = await getArtworkList()
localState.value.finished = finished
}
watch(()=>props.show,(newValue)=>{
if (newValue){
nextTick(()=>{
scrollToCurrentItem()
})
}
})
</script>
<template>
<div>
<x-popup :show="show" @update:show="close">
<template #title>
<div class="text-#000 text-16px">拍品列表</div>
<div class="text-#939393 text-16px ml-14px">{{ pageRef.itemCount }}个拍品</div>
</template>
<div>
<van-pull-refresh
v-model="localState.refreshing"
success-text="刷新成功"
:success-duration="700"
@refresh="onRefresh"
>
<template #success>
<van-icon name="success" /> <span>刷新成功</span>
</template>
<van-list
v-model:loading="storeLoading"
:finished="localState.finished"
finished-text="没有更多了"
@load="loadMore"
>
<div
v-for="(item,index) of itemList"
:key="item.uuid"
class="flex mb-21px item-wrapper"
@click="openShow(item)"
>
<div
class="mr-10px flex-shrink-0 rounded-4px overflow-hidden cursor-pointer relative"
>
<xImage
:preview="false"
class="w-80px h-80px"
:src="item.artwork?.hdPic"
:alt="item?.artworkTitle"
loading="lazy"
/>
<div class="w-45px h-17px bg-#2B53AC text-12px line-height-none flex justify-center items-center absolute top-2px left-2px text-#fff">LOT{{item.index}}</div>
<div v-if="auctionData.artwork.index===item?.index" class="w-80px h-20px bg-#B58047 flex line-height-none justify-center items-center text-#fff text-12px bottom-0 absolute blink">投屏中</div>
</div>
<div>
<div class="ellipsis line-height-20px text-16px font-600 min-h-40px">
{{ item.artworkTitle }}
</div>
<div class="text-14px text-#575757">起拍价RMB 1,000</div>
<div class="text-14px text-#B58047">成交价等待更新</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</x-popup>
<DetailPopup v-model:show="showDetail" :detail-info="showDetailInfo"></DetailPopup>
</div>
</template>
<style scoped>
.ellipsis {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.blink {
animation: fade 1s linear infinite;
}
@keyframes fade {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>

View File

@ -0,0 +1,173 @@
<script setup>
import {ref, onMounted, onBeforeUnmount, watch} from 'vue'
import Aliplayer from 'aliyun-aliplayer'
import 'aliyun-aliplayer/build/skins/default/aliplayer-min.css'
import sideButton from '@/pages/liveRoom/components/SideButton/index.vue'
import broadcast from '@/pages/liveRoom/components/Broadcast/index.vue'
import {liveStore} from "@/stores/live/index.js"
import paymentResults from '@/pages/liveRoom/components/PaymentResults/index.vue'
import paymentInput from '@/pages/liveRoom/components/PaymentInput/index.vue'
import {goodStore} from "@/stores/goods/index.js"
import {message} from "~/components/x-message/useMessage.js"
import {artworkBuy} from "@/api/goods/index.js"
const player = ref(null)
const {quoteStatus, show, playerId, show1, auctionData, getSocketData, getLiveLink,fullLive} = liveStore()
const isPlayerReady = ref(false)
const pullLink = ref('')
definePageMeta({
title: '主页',
i18n: 'login.title',
})
const handlePlayerError = (error) => {
console.error('播放器错误:', error)
player.value?.play()
}
const initializePlayer = async () => {
try {
if (player.value) {
player.value.dispose()
}
const playerConfig = {
id: playerId.value,
source: pullLink.value,
isLive: true,
preload: true,
autoplayPolicy: {fallbackToMute: true},
controlBarVisibility: 'never',
}
player.value = new Aliplayer(playerConfig, (playerInstance) => {
isPlayerReady.value = true
playerInstance?.play()
})
player.value.on('error', handlePlayerError)
} catch (error) {
console.error('播放器初始化失败:', error)
}
}
onMounted(async () => {
pullLink.value = await getLiveLink()
initializePlayer()
})
onBeforeUnmount(() => {
player.value?.dispose()
player.value = null
})
watch(()=>fullLive.value, (newVal) => {
if (newVal) {
getSocketData()
}
})
const goBuy = async () => {
const res = await artworkBuy({
auctionArtworkUuid: auctionData.value?.artwork?.uuid,
buyMoney: String(auctionData.value?.nowAuctionPrice?.nextPrice ?? 0)
})
if (res.status === 0) {
message.success('出价成功')
}
}
const tipOpen = () => {
message.warning('出价状态未开启')
}
const updateShow=()=>{
}
</script>
<template>
<div class="relative h-full">
<div :id="playerId" class="w-full h-full"></div>
<transition>
<div v-if="fullLive">
<sideButton class="absolute top-196px right-0 z-999"></sideButton>
<div class="absolute left-1/2 transform -translate-x-1/2 flex flex-col items-center"
style="bottom:calc(var(--safe-area-inset-bottom) + 26px)">
<div class="text-16px text-#FFB25F font-600">
当前价{{ auctionData?.nowAuctionPrice?.currency }}
<van-rolling-text class="my-rolling-text" :start-num="0" :duration="0.5"
:target-num="auctionData?.nowAuctionPrice?.nowPrice??0" direction="up"/>
</div>
<div class="text-16px text-#fff font-600">
下口价{{ auctionData?.nowAuctionPrice?.currency }}
<van-rolling-text class="my-rolling-text1" :start-num="0" :duration="0.5"
:target-num="auctionData?.nowAuctionPrice?.nextPrice??0" direction="up"/>
</div>
<div v-if="quoteStatus" class="mt-10px mb-10px">
<van-button @click="goBuy" color="#FFB25F" class="w-344px !h-[40px]">
<div>{{
`确认出价 ${auctionData?.nowAuctionPrice?.currency} ${auctionData?.nowAuctionPrice?.nextPrice ?? 0}`
}}</div>
</van-button>
</div>
<div v-else class="mt-10px mb-10px">
<van-button @click="tipOpen" color="#D6D6D8" class="w-344px !h-[40px]" v-if="!quoteStatus">
<div class="text-#7D7D7F text-14px">点击"开启出价"即刻参与竞拍</div>
</van-button>
</div>
<broadcast></broadcast>
</div>
<paymentInput v-model:show="show"/>
<div>
</div>
<paymentResults v-model:show="show1" type="error"/>
<div v-if="auctionData?.wsType==='newArtwork'"
class="w-344px h-31px rounded-4px absolute top-9px bg-[#151824]/45 backdrop-blur-[10px] backdrop-saturate-[180%] left-1/2 transform translate-x--1/2 flex text-#fff text-14px items-center px-12px line-height-none">
<div class="mr-11px whitespace-nowrap">LOT{{ auctionData.artwork.index }}</div>
<div class="mr-10px truncate">{{ auctionData.artwork.name }}</div>
<div class="whitespace-nowrap">开始拍卖</div>
</div>
</div>
</transition>
</div>
</template>
<style lang="scss">
#J_prismPlayer {
width: 100%;
height: 100% !important;
& > video {
width: 100%;
height: 100%;
}
}
</style>
<style scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.my-rolling-text {
--van-rolling-text-item-width: 10px;
--van-rolling-text-font-size: 16px;
--van-rolling-text-color: #FFB25F;
}
.my-rolling-text1 {
--van-rolling-text-item-width: 10px;
--van-rolling-text-font-size: 16px;
--van-rolling-text-color: #FFF;
}
:deep(.prism-license-watermark) {
display: none !important;
}
</style>

209
app/pages/login/index.vue Normal file
View File

@ -0,0 +1,209 @@
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'
import countryCode from '../countryRegion/data/index.js'
import {senCode, userLogin} from "@/api/auth/index.js";
import {authStore} from "@/stores/auth/index.js";
import {message} from '@/components/x-message/useMessage.js'
const {userInfo,token}= authStore()
const router = useRouter();
const route = useRoute();
const { locale } = useI18n()
definePageMeta({
title: '登录',
i18n: 'login.title',
})
const loadingRef=ref({
loading1:false,
loading2:false,
})
const isExist=ref(false)// true
const isReal=ref(false) //isReal
function goToPage() {
router.push('/countryRegion');
}
const interval=ref(null)
const startCountdown=()=> {
if (interval.value){
clearInterval(interval.value);
}
countdown.value = 60;
interval.value = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(interval.value);
}
}, 1000);
}
const countdown = ref(0);
const phoneNum = ref('17630920520')
const code = ref('123789')
const pane = ref(0)
const showKeyboard = ref(false);
//
const getDefaultCountry = () => {
let defaultCode = 'CN' //
switch (locale.value) {
case 'zh-CN':
defaultCode = 'CN'
break
case 'zh-TW':
defaultCode = 'TW'
break
case 'ja-JP':
defaultCode = 'JP'
break
case 'en-US':
defaultCode = 'US'
break
}
const country = countryCode.find(c => c.code === defaultCode)
return {
zone: country.zone,
name: locale.value === 'zh-CN' ? country.cn :
locale.value === 'zh-TW' ? country.tw :
locale.value === 'ja-JP' ? country.ja :
country.en
}
}
const defaultCountry = getDefaultCountry()
//
const selectedZone = ref(route.query.zone || defaultCountry.zone)
const selectedCountry = ref(route.query.countryName || defaultCountry.name)
//
watch(locale, () => {
if (!route.query.zone) {
const newDefault = getDefaultCountry()
selectedZone.value = newDefault.zone
selectedCountry.value = newDefault.name
}
})
const vanSwipeRef=ref(null)
const getCode =async () => {
loadingRef.value.loading1=true
const res=await senCode({
telNum:phoneNum.value,
zone:selectedZone.value
})
loadingRef.value.loading1=false
if (res.status===0){
}
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value=true
startCountdown();
/* pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value=true
startCountdown();*/
}
const goBack = () => {
code.value = ''
pane.value = 0
vanSwipeRef.value?.swipeTo(pane.value)
}
const goLogin =async () => {
loadingRef.value.loading2=true
const res=await userLogin({
telNum:phoneNum.value,
zone:selectedZone.value,
code:code.value
})
if (res.status===0){
userInfo.value=res.data.accountInfo
token.value=res.data.token
if (!res.data.isReal){
await router.push('/realAuth');
}else {
await router.push('/');
}
}
loadingRef.value.loading2=false
}
</script>
<template>
<div class="h-screen-nav w-[100vw] bg-[url('@/static/images/asdfsdd.png')] bg-cover px-[31px] pt-[86px]">
<div class="w-full flex justify-center mb-[100px]">
<img class="h-[105px] w-[189px]" src="@/static/images/ghfggff.png" alt="">
</div>
<van-swipe ref="vanSwipeRef" :show-indicators="false" :touchable="false" :lazy-render="true" :loop="false">
<van-swipe-item >
<div v-show="pane===0">
<div class="">
<div class="w-full flex justify-between" @click="goToPage">
<div class="text-[16px] text-[#000]">
{{ selectedCountry }}
</div>
<div><van-icon color="#777" name="arrow" size="14" /></div>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="phoneNum" clearable :placeholder="$t('login.phonePlaceholder')">
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
+{{ selectedZone }}
</div>
</template>
</van-field>
</div>
<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-else type="primary" color="#D3D3D3" block style="height: 48px">{{ $t('login.getCode')
}}</van-button>
</div>
</div>
</van-swipe-item>
<van-swipe-item>
<div v-show="pane===1">
<div class="flex mb-[16px]">
<div class="text-[16px] text-[#BDBDBD] mr-[10px]">{{ $t('login.hasSendTo') }}</div>
<div class="text-[16px] text-[#000]">+{{ selectedZone }} {{ phoneNum }}</div>
</div>
<van-password-input :value="code" :gutter="10" :mask="false" focused @focus="showKeyboard = true" />
<div :class="`${countdown>0?'text-#BDBDBD':'text-#2B53AC'} text-14px`">
{{ $t('login.reSend') }}<span v-if="countdown>0">({{countdown}})</span>
</div>
<div class="mt-[17px]">
<van-button v-if="code.length === 6" type="primary" block :loading="loadingRef.loading2" :loading-text="$t('login.login')" style="height: 48px" @click="goLogin">{{
$t('login.login')
}}</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">{{ $t('login.login') }}</van-button>
</div>
<div class="mt-[17px]">
<van-button type="primary" @click="goBack" block style="height: 48px">{{ $t('login.back') }}</van-button>
</div>
</div>
</van-swipe-item>
</van-swipe>
<van-number-keyboard v-model="code" :show="showKeyboard" @blur="showKeyboard = false" />
</div>
</template>
<style scoped lang="scss">
:deep(.van-cell.van-field) {
padding-left: 0;
}
:deep(.van-password-input) {
margin: 0;
}
:deep(.van-password-input__item) {
border: 1px solid #E5E5E5;
width: 41px;
height: 41px;
}
</style>

View File

@ -1,13 +1,126 @@
<script setup lang="ts">
<script setup>
import {userArtworks} from "@/api/goods/index.js";
import {authStore} from "@/stores/auth/index.js";
import xImage from '@/components/x-image/index.vue'
import {goodStore} from "~/stores/goods/index.js";
import {ref} from "vue";
definePageMeta({
layout: 'default',
title: '我的',
i18n: 'menu.profile',
})
const {artWorkDetail} = goodStore()
const myList=ref([])
const showMyList=ref([])
const {userInfo}= authStore()
const groupAndSortByDate=(data)=> {
if (!Array.isArray(data)) {
return
}
return Object.values(data.reduce((acc, curr) => {
if (!acc[curr.userCreatedAt]) {
acc[curr.userCreatedAt] = {
userCreatedAt: curr.userCreatedAt,
list: []
}
}
acc[curr.userCreatedAt].list.push(curr)
return acc;
}, {})).sort((a, b) => new Date(b.userCreatedAt) - new Date(a.userCreatedAt));
}
const initData=async ()=>{
const res=await userArtworks({})
if (res.status===0){
myList.value=res.data.data
showMyList.value=groupAndSortByDate(myList.value)
}
}
const router = useRouter()
const localState = ref({
finished: false,
refreshing: false,
showDetail: false,
showHeight: ''
})
initData()
const goPay=()=>{
router.push({
path:'/signature/personal-Info'
})
}
const goDetail=(item)=>{
router.push({
path:'/artDetail',
query:{
uuid:item.uuid
}
})
}
const onRefresh = async () => {
try {
localState.value.refreshing = true
localState.value.finished = false
const { finished } = await getArtworkList(true)
localState.value.finished = finished
} finally {
localState.value.refreshing = false
}
}
</script>
<template>
<div mx-auto mb-60 pt-15 text-center text-16 text-dark dark:text-white>
{{ $t('profile_page.txt') }}
<div class="w-[100vw] bg-[url('@/static/images/3532@2x.png')] bg-cover pt-43px flex-grow-1 flex flex-col">
<div class="flex items-center px-16px mb-43px">
<div class="mr-23px">
<img class="w-57px h-57px" src="@/static/images/5514@2x.png" alt="">
</div>
<div class="flex flex-col">
<div class="text-18px text-#181818">{{userInfo.realName}}</div>
<div class="text-#575757 text-14px">{{userInfo.telNum}}</div>
</div>
</div>
<div class="flex-grow-1 ">
<div class="border-b-1px border-b-#D3D3D3 px-16px flex">
<div class="text-#000 text-16px border-b-3 border-b-#2B53AC h-36px">我的拍品</div>
</div>
<van-pull-refresh v-model="localState.refreshing"
success-text="刷新成功"
:success-duration="700"
@refresh="onRefresh">
<van-list
finished-text="没有更多了"
>
<div class="px-16px pt-14px" v-for="(item,index) of showMyList" >
<div class="text-#575757 text-14px mb-3px">{{item.userCreatedAt}}</div>
<div class="flex mb-22px" v-for="(item1,index1) of item.list" @click="goDetail(item1)">
<div class="flex-shrink-0 mr-10px rounded-4px overflow-hidden">
<x-image class="w-80px h-80px" :src="item1?.auctionArtworkInfo?.artwork?.hdPic" :preview="false" alt=""/>
</div>
<div class="flex flex-col justify-between grow-1">
<div class="text-#000 text-16px ellipsis line-height-21px">{{item1?.auctionArtworkInfo?.artworkTitle}}{{item1?.auctionArtworkInfo?.artworkTitle}}{{item1?.auctionArtworkInfo?.artworkTitle}}</div>
<div class="flex justify-between">
<div>
<div class="text-#575757 text-14px line-height-none mb-5px">起拍价RMB 1,000</div>
<div class="text-#B58047 text-14px line-height-none">成交价RMB 10,000</div>
</div>
<div v-if="[1,3,4].includes(item1.status)" @click.stop="goPay">
<van-button class="w-73px !h-30px" type="primary"><span class="text-12px">去支付</span></van-button>
</div>
</div>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</div>
</template>
<style scoped>
.ellipsis {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -1,42 +0,0 @@
<script lang="ts" setup>
import { useProseStore } from '~/stores/prose'
definePageMeta({
layout: 'default',
title: '随笔',
i18n: 'menu.fetch',
})
const proseStore = useProseStore()
function fetch() {
proseStore.fetchProse()
}
function clear() {
proseStore.clearProse()
}
</script>
<template>
<div>
<div class="h-300 flex items-center justify-center rounded-15 bg-white p-16 dark:bg-[--van-background-2]">
<div v-if="proseStore.prose" class="text-16 leading-26">
{{ proseStore.prose }}
</div>
<ClientOnly v-else>
<van-empty :description="$t('prose_page.btn_empty_desc')" />
</ClientOnly>
</div>
<van-space class="m-10" direction="vertical" fill>
<van-button type="primary" round block @click="fetch">
{{ $t('prose_page.btn_fetch') }}
</van-button>
<van-button type="default" round block @click="clear">
{{ $t('prose_page.btn_clear') }}
</van-button>
</van-space>
</div>
</template>

View File

@ -0,0 +1,65 @@
<script setup>
import {authStore} from "@/stores/auth/index.js";
const props = defineProps({
type: {
type: Number,
default: 0
}
})
const {userInfo}= authStore()
</script>
<template>
<div class="text-#1A1A1A text-16px">
<template v-if="type===0">
<div class="flex mb-20px">
<div class="mr-10px">姓名</div>
<div>{{userInfo.realName}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">性别</div>
<div>{{userInfo.sex}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">出生日期</div>
<div>{{userInfo.birthDate}}</div>
</div>
<div class="flex">
<div class="mr-10px">身份证号</div>
<div>{{userInfo.idNum}}</div>
</div>
</template>
<template v-if="type===1">
<div class="flex mb-20px" >
<div class="mr-10px">姓名</div>
<div>{{userInfo.realName}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">性别</div>
<div>{{userInfo.sex}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">出生日期</div>
<div>{{userInfo.birthDate}}</div>
</div>
<div class="flex">
<div class="mr-10px">家庭住址</div>
<div>{{userInfo.idNum}}</div>
</div>
<div class="flex">
<div class="mr-10px">所属银行</div>
<div>{{userInfo.idNum}}</div>
</div>
<div class="flex">
<div class="mr-10px">银行卡号码</div>
<div>{{userInfo.idNum}}</div>
</div>
</template>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,166 @@
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'
import {userUpdate} from "@/api/auth/index.js";
import {message} from '@/components/x-message/useMessage.js'
import detail from './components/detail.vue'
import {authStore} from "@/stores/auth/index.js";
const router = useRouter();
const route = useRoute();
const showPicker = ref(false);
const {userInfo}= authStore()
const birthdayDate = ref([])
const showBirthdayPicker = ref(false)
const minDate = new Date(1950, 0, 1)
const maxDate = new Date(2025, 12, 31)
const active=ref(0)
const { t } = useI18n()
const form=ref({
idNum: "",
realName: "",
sex:'',
birthDate:'',
userExtend: {
address: "",
bankName: "",
bankNo: ""
}
})
const form1=ref({
idNum:'',
realName:''
})
const columns=ref([
{ text: t('realAuth.male'), value: 1 },
{ text: t('realAuth.female'), value: 2 },
])
const onConfirm = ({ selectedValues, selectedOptions }) => {
form.value.sex=selectedValues?.[0]
showPicker.value = false
}
const onBirthdayConfirm = (value) => {
form.value.birthDate=value.selectedValues.join('-')
showBirthdayPicker.value = false
}
function isFormComplete(obj) {
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
if (!isFormComplete(obj[key])) {
return false;
}
} else if (obj[key] === "") {
return false;
}
}
return true;
}
const statusCode=ref(0)
const confirm=async ()=>{
const thatForm=active.value===0?form1.value:form.value
if (isFormComplete(thatForm)){
const res=await userUpdate(thatForm)
if (res.status===0){
userInfo.value=res.data
message.success('提交成功')
statusCode.value=1
}
}else {
message.error('请填写身份证相关信息')
}
}
const goHome=()=>{
router.push('/')
}
definePageMeta({
title: '实名认证',
i18n: 'realAuth.title',
})
</script>
<template>
<div class="px-[31px] bg-[url('@/static/images/asdfsdd.png')] bg-cover w-100vw flex-grow-1 pt-[46px] relative flex flex-col">
<van-tabs v-if="statusCode===0" v-model:active="active" animated swipeable>
<van-tab :title="$t('realAuth.cnTab')" class="pt-[80px]">
<template v-if="statusCode===0">
<div class="text-[#BDBDBD] text-[16px] mb-[34px]">{{ $t('realAuth.cnTabDesc') }}</div>
<div class="mb-[100px]">
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form1.idNum" :label="$t('realAuth.idCard')" clearable
:placeholder="$t('realAuth.idCardPlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form1.realName" :label="$t('realAuth.name')" clearable :placeholder="$t('realAuth.namePlaceholder')"></van-field>
</div>
</div>
</template>
</van-tab>
<van-tab :title="$t('realAuth.otherTab')" class="pt-[80px]">
<div class="text-[#BDBDBD] text-[16px] mb-[34px]">{{ $t('realAuth.otherTabDesc') }}</div>
<div class="mb-[100px]">
<div class="border-b-[1.7px] mt-[8px]">
<van-field :label="$t('realAuth.name')" clearable :placeholder="$t('realAuth.namePlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field :modelValue="columns.find(x=>x.value===form.sex)?.text" is-link readonly name="picker" :label="$t('realAuth.gender')"
@click="showPicker = true" />
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form.birthDate" is-link readonly name="birthdayPicker" :label="$t('realAuth.birthday')"
:placeholder="$t('realAuth.birthdayPlaceholder')" @click="showBirthdayPicker = true" />
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form.userExtend.address" :label="$t('realAuth.adress')" clearable
:placeholder="$t('realAuth.adressPlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form.userExtend.bankName" :label="$t('realAuth.bank')" clearable :placeholder="$t('realAuth.bankPlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form.userExtend.bankNo" :label="$t('realAuth.bankCard')" clearable
:placeholder="$t('realAuth.bankCardPlaceholder')"></van-field>
</div>
</div>
</van-tab>
</van-tabs>
<van-tabs v-else-if="statusCode===1" v-model:active="active" animated swipeable>
<van-tab :title="$t('realAuth.cnTab')" class="pt-[80px]">
<detail :type="active"></detail>
</van-tab>
<van-tab :title="$t('realAuth.otherTab')" class="pt-[80px]">
<detail :type="active"></detail>
</van-tab>
</van-tabs>
<div class="flex justify-between" v-if="statusCode===0">
<van-button style="width: 151px;height: 48px" color="#E9F1F8">
<div class="text-#2B53AC text-16px">{{ $t('realAuth.cancel') }}</div>
</van-button>
<van-button @click="confirm" style="width: 151px;height: 48px" color="#2B53AC">
<div class="text-#FFFFFF text-16px">{{ $t('realAuth.confirm') }}</div>
</van-button>
</div>
<div v-else class="mt-auto pb-94px">
<van-button color="#E9F1F8" @click="goHome" style="color: #2B53AC;font-weight: 600" block>去首页</van-button>
</div>
<van-popup v-model:show="showPicker" destroy-on-close position="bottom">
<van-picker :columns="columns" @confirm="onConfirm" @cancel="showPicker = false" />
</van-popup>
<van-popup v-model:show="showBirthdayPicker" destroy-on-close position="bottom">
<van-date-picker v-model="birthdayDate" :min-date="minDate" :max-date="maxDate"
@cancel="showBirthdayPicker = false" @confirm="onBirthdayConfirm" />
</van-popup>
</div>
</template>
<style scoped>
:deep(.van-tabs__line) {
height: 2px;
width: 107px;
}
:deep(.van-cell) {
padding-left: 0;
}
</style>

View File

@ -0,0 +1,18 @@
<script setup>
const image = ref('');
import { showToast } from 'vant';
const onSubmit = (data) => {
image.value = data.image;
};
const onClear = () => showToast('clear');
</script>
<template>
<van-signature @submit="onSubmit" @clear="onClear" />
<van-image v-if="image" :src="image" />
</template>
<style scoped>
</style>

View File

@ -0,0 +1,63 @@
<script setup>
import {useI18n} from "vue-i18n";
import XVanSelect from '@/components/x-van-select/index.vue'
import XVanDate from '@/components/x-van-date/index.vue'
definePageMeta({
name: 'personal-info',
})
const {t} = useI18n()
const showPicker = ref(false)
const showPicker1 = ref(false)
const onConfirm = () => {
}
const router = useRouter()
const columns = ref([
{text: t('realAuth.male'), value: 1},
{text: t('realAuth.female'), value: 2},
])
const goCountryRegion=()=>{
router.push({
path:'/countryRegion'
})
}
const adress=ref('')
</script>
<template>
<div
class="w-[100vw] bg-[url('@/static/images/asdfsdd.png')] h-screen-nav bg-cover pt-77px flex-grow-1 flex flex-col ">
<div class="text-16px text-#191919 font-bold mb-40px px-34px">
请填写个人相关信息
</div>
<div class="grow-1 px-34px">
<van-field type="tel" :label-width="161" label="文本" class="mb-10px" placeholder="请输入手机号">
<template #label>
<div class="flex">
<div class="mr-41px whitespace-nowrap">手机号</div>
<div @click="goCountryRegion">
<span class="mr-13px">+ 86</span>
<van-icon name="arrow-down" class="text-#777777"/>
</div>
</div>
</template>
</van-field>
<van-field label="姓名" class="mb-10px" placeholder="请输入姓名"/>
<x-van-select label="性别" :columns="columns"/>
<x-van-date label="出生日期"/>
<van-field v-model="adress" label="家庭住址" class="mb-10px" placeholder="请输入家庭住址"/>
<van-field label="所属银行" class="mb-10px" placeholder="请输入所属银行"/>
<van-field label="银行卡号码" class="mb-10px" placeholder="请输入银行卡号码"/>
</div>
<div class="h-81px bg-#fff flex justify-center pt-7px border-t">
<van-button color="#2B53AC" class="w-213px van-btn-h-38px">下一步</van-button>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.van-cell.van-field){
padding-left: 0;
}
</style>

View File

@ -0,0 +1,43 @@
<script setup>
const activeNames = ref(['1']);
definePageMeta({
layout: 'default',
title: '签署内容'
})
</script>
<template>
<div class="bg-#EBEBEB h-screen-nav flex flex-col">
<div class="h-50px text-14px text-#191919 bg-#fff flex items-center px-21px mb-6px">支付前需同意以下内容并签字</div>
<van-collapse v-model="activeNames" class="grow-1">
<van-collapse-item name="1" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
<van-collapse-item name="2" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
<van-collapse-item name="3" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
</van-collapse>
<div class="h-81px bg-#fff flex justify-center pt-7px border-t">
<van-button color="#2B53AC" class="w-213px van-btn-h-38px">同意并签字</van-button>
</div>
</div>
</template>
<style scoped>
:deep(.van-cell__right-icon){
color: #ACACAC;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import MasonryWall from '@yeger/vue-masonry-wall'
const items = [
{
title: 'First',
description: 'The first item.',
},
{
title: 'Second',
description: 'The second item.',
},
]
</script>
<template>
<masonry-wall :items="items" :ssr-columns="2" :minColumns="2" :gap="16">
<template #default="{ item, index }">
<div :style="{ height: `${(index + 1) * 100}px` }">
<h1>{{ item.title }}</h1>
<span>{{ item.description }}</span>
</div>
</template>
</masonry-wall>
</template>

View File

@ -1,22 +0,0 @@
<script setup lang="ts">
definePageMeta({
title: '🎨 Unocss 示例',
i18n: 'menu.unocssExample',
})
</script>
<template>
<div>
<h1 class="text-6xl color-pink font-semibold">
{{ $t('unocss_page.hello', ['Unocss!']) }}
</h1>
<p class="mt-10 text-gray-700 dark:text-white">
{{ $t('unocss_page.desc') }}
</p>
<button class="mt-10 btn">
{{ $t('unocss_page.btn_txt') }}
</button>
</div>
</template>

View File

@ -1,4 +1,4 @@
import { setupHttp } from '~/api/http'
import { setupHttp } from '@/api/http'
export default defineNuxtPlugin(() => {
setupHttp()

View File

@ -2,11 +2,15 @@ import type { Locale as TypeLocale } from '#i18n'
import { Locale } from 'vant'
import enUS from 'vant/es/locale/lang/en-US'
import zhCN from 'vant/es/locale/lang/zh-CN'
import jaJP from 'vant/es/locale/lang/ja-JP'
import zhTW from 'vant/es/locale/lang/zh-TW'
export default defineNuxtPlugin(() => {
// 载入 vant 语言包
Locale.use('zh-CN', zhCN)
Locale.use('en-US', enUS)
Locale.use('ja-JP', jaJP)
Locale.use('zh-TW', zhTW)
if (import.meta.client) {
const i18n = useNuxtApp().$i18n

View File

@ -0,0 +1,8 @@
import VConsole from 'vconsole'
export default defineNuxtPlugin(() => {
if (process.env.NODE_ENV !== 'production') {
const vConsole = new VConsole()
console.log('VConsole is enabled')
}
})

89
app/plugins/websocket.ts Normal file
View File

@ -0,0 +1,89 @@
import {authStore} from "@/stores/auth";
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const { token } = authStore()
const ws = reactive({
instance: null as WebSocket | null,
isConnected: false,
// 修改 connect 方法接收路径和数据对象
connect(path: string, data?: Record<string, any>) {
if (this.instance?.readyState === WebSocket.OPEN) {
this.instance.close()
}
// 构建查询字符串
const queryString =data
? '?' + Object.entries({ token: token.value,...data})
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
: ''
// 构建完整的 WebSocket URL
const wsUrl = `${config.public.NUXT_PUBLIC_SOCKET_URL}${path}${queryString}`
this.instance = new WebSocket(wsUrl)
this.instance.onopen = () => {
this.isConnected = true
console.log('WebSocket 已连接')
}
this.instance.onclose = () => {
this.isConnected = false
console.log('WebSocket 已断开')
/* this.reconnect(path, data)*/
}
this.instance.onerror = (error) => {
console.error('WebSocket 错误:', error)
}
this.instance.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.handleMessage(data)
} catch (error) {
console.error('消息解析错误:', error)
}
}
},
// 更新重连方法以支持数据对象
reconnect(path: string, data?: Record<string, any>) {
setTimeout(() => {
console.log('尝试重新连接...')
this.connect(path, data)
}, 3000)
},
// 发送消息
send(data: any) {
if (this.instance?.readyState === WebSocket.OPEN) {
this.instance.send(JSON.stringify(data))
} else {
console.warn('WebSocket 未连接,无法发送消息')
}
},
// 关闭连接
disconnect() {
if (this.instance) {
this.instance.close()
this.instance = null
}
},
// 消息处理
handleMessage(data: any) {
// 触发自定义事件,让组件可以监听
window.dispatchEvent(new CustomEvent('ws-message', { detail: data }))
}
})
return {
provide: {
ws
}
}
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Some files were not shown because too many files have changed in this diff Show More