Merge branch 'xingyy' into dev
# Conflicts: # app/components/AppFooter.vue
21
LICENSE
@ -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.
|
|
@ -39,9 +39,6 @@
|
|||||||
- 🔥 `<script setup>` 语法
|
- 🔥 `<script setup>` 语法
|
||||||
|
|
||||||
- 🌍 [国际化支持](./i18n/locales)
|
- 🌍 [国际化支持](./i18n/locales)
|
||||||
|
|
||||||
- 🍍 [使用 Pinia 进行状态管理](https://github.com/vuejs/pinia),查看 [./app/composables/counter.ts](./app/composables/counter.ts)
|
|
||||||
|
|
||||||
- 📑 [布局系统](./app/layouts)
|
- 📑 [布局系统](./app/layouts)
|
||||||
|
|
||||||
- 📥 API 自动导入 - 用于 Composition API 和自定义组合式函数
|
- 📥 API 自动导入 - 用于 Composition API 和自定义组合式函数
|
||||||
@ -57,8 +54,6 @@
|
|||||||
- [i18n](https://github.com/nuxt-modules/i18n) - Nuxt 的国际化模块
|
- [i18n](https://github.com/nuxt-modules/i18n) - Nuxt 的国际化模块
|
||||||
- [ColorMode](https://github.com/nuxt-modules/color-mode) - 支持自动检测的深色和浅色模式
|
- [ColorMode](https://github.com/nuxt-modules/color-mode) - 支持自动检测的深色和浅色模式
|
||||||
- [UnoCSS](https://github.com/unocss/unocss) - 即时按需原子化 CSS 引擎
|
- [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 开发者体验
|
- [DevTools](https://github.com/nuxt/devtools) - 释放 Nuxt 开发者体验
|
||||||
|
|
||||||
## IDE
|
## IDE
|
||||||
|
23
app/api-collect-code/auth/index.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
31
app/api-collect-code/goods/index.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
128
app/api-collect-code/http.js
Normal 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
@ -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
@ -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
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { getHttp } from './http'
|
|
||||||
|
|
||||||
export async function getProse() {
|
|
||||||
const http = getHttp()
|
|
||||||
return await http('/api/prose', {
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
}
|
|
88
app/app.vue
@ -1,28 +1,86 @@
|
|||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import type { ConfigProviderTheme } from 'vant'
|
import {useI18n} from 'vue-i18n'
|
||||||
import useKeepalive from '~/composables/keepalive'
|
import {message} from '@/components/x-message/useMessage.js'
|
||||||
import { appName } from '~/constants'
|
// message.success('success')
|
||||||
|
|
||||||
useHead({
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VanConfigProvider :theme="mode">
|
<VanConfigProvider>
|
||||||
<NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
|
<NuxtLoadingIndicator
|
||||||
|
color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)"/>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage :keepalive="{ include: keepAliveRouteNames }" />
|
<NuxtPage :transition="{
|
||||||
|
name: slideDirection
|
||||||
|
}"/>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</VanConfigProvider>
|
</VanConfigProvider>
|
||||||
</template>
|
</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>
|
@ -1,30 +1,38 @@
|
|||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import { useAppFooterRouteNames as names } from '~/config'
|
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 route = useRoute()
|
||||||
|
|
||||||
const active = ref(0)
|
const active = ref(0)
|
||||||
|
|
||||||
const show = computed(() => {
|
const show = computed(() => {
|
||||||
if (route.name && names.includes(route.name))
|
if (route.name && names.includes(route.name))
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
const initData=()=>{
|
||||||
|
active.value=route.path==='/profile'?1:0
|
||||||
|
}
|
||||||
|
watchEffect(initData)
|
||||||
|
onMounted(()=>{
|
||||||
|
initData()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<van-tabbar v-if="show" v-model="active" route placeholder fixed>
|
<div v-if="show" v-memo="[active]" >
|
||||||
<van-tabbar-item replace to="/">
|
<van-tabbar v-model="active" route placeholder fixed>
|
||||||
<span>{{ $t('tabbar.home') }}</span>
|
<van-tabbar-item replace to="/">
|
||||||
<template #icon>
|
<span>{{ $t('tabbar.home') }}</span>
|
||||||
<div class="i-carbon:home" />
|
<template #icon>
|
||||||
</template>
|
<HomeIcon :active="active===0"></HomeIcon>
|
||||||
</van-tabbar-item>
|
</template>
|
||||||
<van-tabbar-item replace to="/profile">
|
</van-tabbar-item>
|
||||||
<span>{{ $t('tabbar.profile') }}</span>
|
<van-tabbar-item replace to="/profile">
|
||||||
<template #icon>
|
<span>{{ $t('tabbar.profile') }}</span>
|
||||||
<div class="i-carbon:user" />
|
<template #icon>
|
||||||
</template>
|
<MyIcon :active="active===1"></MyIcon>
|
||||||
</van-tabbar-item>
|
</template>
|
||||||
</van-tabbar>
|
</van-tabbar-item>
|
||||||
|
</van-tabbar>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import { useAppFooterRouteNames as routeWhiteList } from '~/config'
|
import { useAppHeaderRouteNames as routeWhiteList } from '@/config'
|
||||||
|
import { liveStore } from "@/stores/live/index.js";
|
||||||
|
const { fullLive } = liveStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
function onBack() {
|
function onBack() {
|
||||||
|
if (fullLive.value){
|
||||||
|
fullLive.value=false
|
||||||
|
return
|
||||||
|
}
|
||||||
if (window.history.state.back)
|
if (window.history.state.back)
|
||||||
history.back()
|
history.back()
|
||||||
else
|
else
|
||||||
@ -14,19 +18,34 @@ function onBack() {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
|
|
||||||
if (!route.meta)
|
if (!route.meta)
|
||||||
return ''
|
return ''
|
||||||
return route.meta.i18n ? t(route.meta.i18n) : (route.meta.title || '')
|
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))
|
const showLeftArrow = computed(() => route.name && routeWhiteList.includes(route.name))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VanNavBar
|
<VanNavBar
|
||||||
|
v-memo="[title,fullLive,showLeftArrow,subTitle]"
|
||||||
:title="title"
|
:title="title"
|
||||||
:left-arrow="!showLeftArrow"
|
:left-arrow="!showLeftArrow||fullLive"
|
||||||
placeholder clickable fixed
|
placeholder clickable fixed
|
||||||
@click-left="onBack"
|
@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>
|
</template>
|
||||||
|
181
app/components/SignaturePad.vue
Normal 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>
|
15
app/components/icons/HomeIcon.vue
Normal 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>
|
15
app/components/icons/MyIcon.vue
Normal 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>
|
BIN
app/components/icons/images/home1.png
Normal file
After Width: | Height: | Size: 864 B |
BIN
app/components/icons/images/home2.png
Normal file
After Width: | Height: | Size: 964 B |
BIN
app/components/icons/images/my1.png
Normal file
After Width: | Height: | Size: 1015 B |
BIN
app/components/icons/images/my2.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
55
app/components/itemDetail/index.vue
Normal 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>
|
183
app/components/liveMinWindow/index.vue
Normal 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>
|
28
app/components/x-button/index.vue
Normal 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>
|
52
app/components/x-image/index.vue
Normal 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>
|
BIN
app/components/x-message/images/error.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app/components/x-message/images/info.png
Normal file
After Width: | Height: | Size: 932 B |
BIN
app/components/x-message/images/success.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
app/components/x-message/images/warning.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
93
app/components/x-message/index.vue
Normal 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>
|
121
app/components/x-message/message/index.vue
Normal 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>
|
78
app/components/x-message/useMessage.js
Normal 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 }
|
57
app/components/x-popup/index.vue
Normal 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>
|
109
app/components/x-van-date/index.vue
Normal 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>
|
88
app/components/x-van-select/index.vue
Normal 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>
|
@ -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
|
|
@ -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
|
|
21
app/composables/useWebSocket.ts
Normal 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
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
export const useAppFooterRouteNames= ['index', 'profile']
|
||||||
|
export const useAppHeaderRouteNames= ['index', 'profile','login','collectCode-login','collectCode-mine']
|
@ -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
2
app/constants/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const appName = '豐和'
|
||||||
|
export const appDescription = '泰丰国际京都拍卖会'
|
@ -1,2 +0,0 @@
|
|||||||
export const appName = 'nuxt-vant-mobile'
|
|
||||||
export const appDescription = 'Nuxt H5 Starter Template'
|
|
@ -7,7 +7,7 @@ By default, `default.vue` will be used unless an alternative is specified in the
|
|||||||
```vue
|
```vue
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'home',
|
layout: 'goods',
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="flex flex-col min-h-svh">
|
<main class="flex flex-col min-h-svh">
|
||||||
<AppHeader class="h-[var(--van-nav-bar-height)]" />
|
<AppHeader class="h-[var(--van-nav-bar-height)]" />
|
||||||
|
<div class="flex-1 flex flex-col">
|
||||||
<div class="flex-1 p-16 pb-[var(--van-nav-bar-height)]">
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
<AppFooter />
|
||||||
<AppFooter />
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup >
|
||||||
|
</script>
|
@ -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)
|
|
||||||
})
|
|
101
app/pages/artDetail/index.vue
Normal 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>
|
208
app/pages/collectCode/login/index.vue
Normal 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>
|
72
app/pages/collectCode/mine/components/codeCard/index.vue
Normal 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>
|
210
app/pages/collectCode/mine/index.vue
Normal 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>
|
43
app/pages/collectCode/payment/index.vue
Normal 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>
|
18
app/pages/collectCode/signature/panel/index.vue
Normal 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>
|
58
app/pages/collectCode/signature/personal-Info/index.vue
Normal 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>
|
40
app/pages/collectCode/signature/protocol/index.vue
Normal 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>
|
@ -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>
|
|
1515
app/pages/countryRegion/data/index.js
Normal file
186
app/pages/countryRegion/index.vue
Normal 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>
|
19
app/pages/home/components/Cescribe/index.vue
Normal 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>
|
28
app/pages/home/components/DetailPopup/index.vue
Normal 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>
|
116
app/pages/home/components/ItemList/index.vue
Normal 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
@ -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>
|
@ -1,95 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import type { LocaleObject } from '@nuxtjs/i18n'
|
import Home from './home/index.vue'
|
||||||
import type { PickerColumn } from 'vant'
|
|
||||||
import type { ComputedRef } from 'vue'
|
|
||||||
import { Locale } from 'vant'
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'default',
|
layout: 'default',
|
||||||
title: '主页',
|
title: '主页',
|
||||||
i18n: 'menu.home',
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<Home/>
|
||||||
<VanCellGroup inset>
|
</template>
|
||||||
<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>
|
|
||||||
</template>
|
|
@ -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>
|
<template>
|
||||||
<div>
|
<div class="h-[100vh] w-[100vw]">
|
||||||
<p> {{ $t('keepalive_page.label') }} </p>
|
<SignaturePad v-model="signature" @change="handleSignatureChange"/>
|
||||||
<van-stepper v-model="value" class="mt-10" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
62
app/pages/liveRoom/components/Broadcast/index.vue
Normal 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>
|
92
app/pages/liveRoom/components/PaymentInput/index.vue
Normal 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>
|
42
app/pages/liveRoom/components/PaymentResults/index.vue
Normal 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>
|
68
app/pages/liveRoom/components/SideButton/index.vue
Normal 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>
|
150
app/pages/liveRoom/components/SideButton/tangPopup.vue
Normal 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>
|
173
app/pages/liveRoom/index.client.vue
Normal 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
@ -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>
|
@ -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({
|
definePageMeta({
|
||||||
layout: 'default',
|
layout: 'default',
|
||||||
title: '我的',
|
title: '我的',
|
||||||
i18n: 'menu.profile',
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div mx-auto mb-60 pt-15 text-center text-16 text-dark dark:text-white>
|
<div class="w-[100vw] bg-[url('@/static/images/3532@2x.png')] bg-cover pt-43px flex-grow-1 flex flex-col">
|
||||||
{{ $t('profile_page.txt') }}
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.ellipsis {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -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>
|
|
65
app/pages/realAuth/components/detail.vue
Normal 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>
|
166
app/pages/realAuth/index.vue
Normal 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>
|
18
app/pages/signature/panel/index.vue
Normal 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>
|
63
app/pages/signature/personal-Info/index.vue
Normal 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>
|
43
app/pages/signature/protocol/index.vue
Normal 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>
|
24
app/pages/test/index.client.vue
Normal 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>
|
@ -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>
|
|
@ -1,4 +1,4 @@
|
|||||||
import { setupHttp } from '~/api/http'
|
import { setupHttp } from '@/api/http'
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
setupHttp()
|
setupHttp()
|
||||||
|
@ -2,11 +2,15 @@ import type { Locale as TypeLocale } from '#i18n'
|
|||||||
import { Locale } from 'vant'
|
import { Locale } from 'vant'
|
||||||
import enUS from 'vant/es/locale/lang/en-US'
|
import enUS from 'vant/es/locale/lang/en-US'
|
||||||
import zhCN from 'vant/es/locale/lang/zh-CN'
|
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(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
// 载入 vant 语言包
|
// 载入 vant 语言包
|
||||||
Locale.use('zh-CN', zhCN)
|
Locale.use('zh-CN', zhCN)
|
||||||
Locale.use('en-US', enUS)
|
Locale.use('en-US', enUS)
|
||||||
|
Locale.use('ja-JP', jaJP)
|
||||||
|
Locale.use('zh-TW', zhTW)
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
const i18n = useNuxtApp().$i18n
|
const i18n = useNuxtApp().$i18n
|
||||||
|
8
app/plugins/vconsole.client.js
Normal 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
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
BIN
app/static/images/1981736313.png
Normal file
After Width: | Height: | Size: 283 KiB |
BIN
app/static/images/3532@2x.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
app/static/images/5514@2x.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
app/static/images/5532@2x.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
app/static/images/asdfsdd.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
app/static/images/close@2x.png
Normal file
After Width: | Height: | Size: 818 B |
BIN
app/static/images/dasfsa.png
Normal file
After Width: | Height: | Size: 293 KiB |
BIN
app/static/images/dddf34@2x.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
app/static/images/ddfdcaer.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
app/static/images/delete3@.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
app/static/images/ghfggff.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
app/static/images/icon-design-42@3x.png
Normal file
After Width: | Height: | Size: 686 B |
BIN
app/static/images/lock4@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/static/images/lockdfd@2x.png
Normal file
After Width: | Height: | Size: 995 B |
BIN
app/static/images/logout.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
app/static/images/qrcodetext.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
app/static/images/zd5530@2x.png
Normal file
After Width: | Height: | Size: 20 KiB |