Compare commits

..

56 Commits

Author SHA1 Message Date
xingyy
23a127b0d3 12 2025-03-03 09:20:27 +08:00
xingyy
839dbe8a42 12 2025-03-03 09:13:12 +08:00
xingyy
ac25fe40dc 12123 2025-03-02 18:05:05 +08:00
xingyy
fb6cb233a1 12123 2025-03-02 17:31:04 +08:00
xingyy
a79f1b85da 12123 2025-03-02 17:21:17 +08:00
xingyy
2b13718cea 12 2025-03-02 14:57:15 +08:00
xingyy
575fcae049 12 2025-03-02 14:43:17 +08:00
xingyy
53571882a0 refactor(auth): 重构登录模块并优化验证码功能
- 修改 token 在 codeAuthStore 中的名称,从 token 改为 codeToken
- 优化验证码发送逻辑,增加错误处理和加载状态管理
- 调整页面布局和样式,提高用户体验
- 修复了一些小的 UI 问题,例如输入框聚焦状态和倒计时显示
2025-03-02 14:35:00 +08:00
xingyy
8207170a01 feat(login): 优化登录页面验证码输入体验
- 移除虚拟键盘,改用原生输入框
- 添加输入框焦点管理,提升用户体验
- 优化验证码输入框样式和功能
- 改进登录流程中的页面切换逻辑
2025-03-02 14:10:24 +08:00
xingyy
9688e9b3a6 feat(format): 优化数字格式化和登录页面布局
- 在 ItemList 组件中添加 formatNumber 函数,用于将数字格式化为 "250XX" 格式
- 调整 login 页面背景图片布局,使其居底显示
- 优化 login 页面验证码输入框,提高用户体验
- 移除 nuxt.config.js 中的 https 配置项
2025-03-02 13:57:52 +08:00
xingyy
e0af9f99a0 refactor(websocket): 完善 WebSocket 客户端 token 处理和错误日志
- 在 WebSocketClient 构造函数中存储 token
- 自动在连接参数中添加 token
- 添加 WebSocket 消息解析错误的控制台日志
2025-03-02 13:23:45 +08:00
xingyy
5d814a3922 refactor(payment): 移除冗余的变量赋值
- 删除了不必要的 checkoutSessionUrl 和 payUid 变量赋值
-优化了代码结构,提高了代码的可读性和维护性
2025-03-02 11:33:53 +08:00
xingyy
33713932a1 refactor: 删除 Stripe 支付页面
- 移除了整个 Stripe 支付页面的代码,包括模板、脚本和样式
- 删除了与支付相关的逻辑和 API 调用
- 移除了对 Stripe 库的引用和配置
2025-03-02 11:23:06 +08:00
xingyy
e7cd4ff9ba refactor(payment): 重构支付流程和页面
- 移除 authStore 中的 payUid 和 checkoutSessionUrl
- 更新 checkoutPage 组件,使用路由参数传递支付相关信息- 修改 payment 页面逻辑,适应新的支付流程
- 删除未使用的 payment/checkoutPage 组件
2025-03-02 11:19:45 +08:00
xingyy
a0d3b2b329 123 2025-03-02 11:09:06 +08:00
xingyy
779dc84356 refactor(websocket): 重构 WebSocket 连接和管理
- 在 checkoutPage 中添加全局 WebSocket 客户端实例
- 移除 payment 中的 openRefreshResults 函数
- 更新 authStore,移除不再使用的 openRefreshResults
- 优化 WebSocketClient 类,移除冗余的 token 参数
- 在 nuxt.config.js 中添加 SSL 证书加载逻辑
2025-03-02 10:58:24 +08:00
xingyy
b1d2dc19d2 feat(dev): 添加本地 HTTPS 证书并配置 Nuxt 服务器
- 新增本地 HTTPS 证书文件:localhost.pem 和 localhost-key.pem
- 在 nuxt.config.js 中配置 HTTPS 选项,启用本地 HTTPS
- 优化 checkoutPage 中的支付流程,使用 router.replace 替代 window.location.href
2025-03-02 10:36:36 +08:00
xingyy
2fdcfc8c3b 1212 2025-03-02 09:54:41 +08:00
xingyy
4b7a7ce285 feat(collectCode): 更新支付功能
- 使用 codeAuthStore 替代 authStore
- 修改支付相关变量和路由
- 在支付成功
2025-02-28 20:42:21 +08:00
xingyy
09bbd9ae14 12 2025-02-28 20:30:45 +08:00
xingyy
437e74ee86 1212 2025-02-28 19:29:43 +08:00
xingyy
38fc5d78ca 1212 2025-02-28 17:40:57 +08:00
xingyy
a84e1524b5 1212 2025-02-28 17:38:00 +08:00
xingyy
be60cac47e 1212 2025-02-28 17:34:13 +08:00
xingyy
46d17fb6e6 12 2025-02-28 17:32:38 +08:00
xingyy
da43bb5429 12 2025-02-28 16:56:37 +08:00
xingyy
1263128171 12 2025-02-28 16:50:45 +08:00
xingyy
c32869988c 123 2025-02-28 16:49:06 +08:00
xingyy
1ed311c5f9 123 2025-02-28 16:41:12 +08:00
xingyy
bbb191651e refactor(payment): 重构支付页面 URL
- 引入 useRuntimeConfig() 以获取环境变量- 使用 Nuxt 配置中的 API_BASE_URL 替代 window.location.origin
- 更新 return_url 为绝对路径,确保在不同环境中的一致性
2025-02-28 16:15:53 +08:00
xingyy
bed0e6c4be fix(payment): 修复支付结果页面的轮询逻辑和返回 URL
- 更新了 Checkout 页面的返回 URL,使用动态获取的窗口位置替代硬编码的 IP 地址
- 优化了支付结果页面的轮询逻辑:
  - 增加了对 5 秒轮询时间的检查,避免长时间轮询
  - 调整了轮询停止的条件,只在支付成功时停止轮询
- 对支付结果页面的 HTML 结构进行了轻微的调整,提高了代码的可读性
2025-02-28 15:59:44 +08:00
xingyy
f973f04742 ci(env): 添加生产环境公钥 2025-02-28 15:33:10 +08:00
xingyy
ee720107fa refactor(i18n): 更新多语言文案并添加新字段
- 在多个语言文件中添加新的翻译字段 idTypeString 和 incompleteForm
- 修改个人身份信息页面中的证件类型文本
- 更新错误提示信息
2025-02-28 15:29:00 +08:00
xingyy
28679f3627 refactor(i18n): 优化多语言文案表述
- 修改身份证相关文案,使其更加通用和国际化
- 统一使用"ID number"、"证件号"等表述,替代"ID Card Number"、"身份证号"
- 调整部分文案以提高可读性和一致性
2025-02-28 15:14:36 +08:00
xingyy
d1579247e1 feat(payment): 集成 Stripe 支付功能
- 新增 Stripe 支付相关的组件和页面
- 实现了支付流程的初始化、表单提交和结果处理
- 优化了支付页面的样式和交互
- 更新了部分 API 接口以支持新的支付功能
2025-02-28 15:08:48 +08:00
xingyy
70fa0eb135 feat(payment): 添加 Stripe 支付功能
- 新增 Stripe 支付页面和相关资源
- 实现支付流程和状态展示
- 添加错误处理和重试功能
2025-02-28 11:41:34 +08:00
xingyy
f03c312b38 feat(realAuth): 添加证件类型和号码功能
- 在实名认证页面添加证件类型和号码输入字段
- 更新国际化文件,添加相关翻译
- 修改个人资料页面,增加证件类型和号码显示
- 更新收集码签名页面,调整身份证件类型选择
2025-02-28 11:14:57 +08:00
xingyy
5e9df53433 12 2025-02-28 10:40:42 +08:00
xingyy
b1b20a9a33 12 2025-02-28 10:35:26 +08:00
xingyy
0fedce8232 12 2025-02-28 10:10:57 +08:00
xingyy
436ebe3166 12 2025-02-28 10:08:17 +08:00
xingyy
a11700a15a fix(goods): 修复首页和直播室页面的问题
- 在 liveRoom 页面中添加 getAuctionDetail 调用,以获取拍卖详情
- 在 home 页面中移除不必要的 getAuctionDetail
2025-02-27 16:55:41 +08:00
xingyy
3db1666c04 12 2025-02-27 16:51:52 +08:00
xingyy
772b058270 feat(i18n): 添加新的翻译文本并更新首页显示
- 在 en-US、ja-JP、zh-CN 和 zh-TW 语言文件中添加了新的翻译文本- 更新了首页拍卖状态的显示逻辑,使用新加的翻译文本- 优化了代码结构,提高了可维护性
2025-02-27 16:41:17 +08:00
xingyy
05b7b42e00 12 2025-02-27 16:32:09 +08:00
xingyy
812135c5bb refactor(app): 重构应用初始化和骨架屏逻辑
- 移除了不必要的 preload 脚本- 优化了 i18n 和 websocket 插件的实现- 简化了 app.vue 中的逻辑
- 新增了独立的骨架屏组件
2025-02-27 16:25:31 +08:00
xingyy
6cb7a282f6 refactor(app): 优化代码结构和依赖
- 移除了未使用的 countUp 相关代码- 清理了无用的引用和变量
- 调整了拍卖数据的展示方式- 删除了未使用的 postcss-mobile-forever 依赖
2025-02-27 16:02:25 +08:00
xingyy
7b56c8543a 12 2025-02-27 15:41:12 +08:00
xingyy
950625f880 12 2025-02-27 15:00:46 +08:00
xingyy
462a2b23a9 feat(i18n): 实现国际化支持并优化 WebSocket 连接
- 移除 App.vue 中的固定语言设置
- 在 i18n.ts 中实现系统语言自动检测和手动设置支持
- 添加语言切换时重新连接 WebSocket 的逻辑
- 在 profile 页面添加语言设置入口
- 优化 WebSocket连接过程,添加 accept-language头
2025-02-27 14:38:03 +08:00
xingyy
c524bf34f5 refactor(i18n):修复国际化设置并启用自动检测语言功能
- 恢复了自动检测系统语言的逻辑
- 注释掉了固定的中文语言设置- 在 externallinks 页面中更新了 iframe 的样式
2025-02-27 13:47:40 +08:00
xingyy
ee2c01fece refactor(payment): 优化支付结果页面并添加回到首页功能
- 在支付结果页面添加回到首页按钮
- 更新国际化文件,增加回到首页翻译
- 优化签名功能相关页面代码
2025-02-27 09:46:51 +08:00
xingyy
df597c0328 feat(floatingBubble): 添加国际化支持
- 在 floating.js 中集成 vue-i18n
- 从 nuxt/app 导入 useNuxtApp 替代 useI18n
- 更新 index.vue 中的国际化使用方式
2025-02-27 09:23:01 +08:00
xingyy
d9d52ab7cb infrastructure(env): 更新生产环境 API 和 WebSocket URL
- 将 API 基础 URL 和 WebSocket URL 从 https://auction.yixunlink.com 更改为 https://auction.szjixun.cn
- 修正签名提示文本的翻译键
2025-02-27 09:15:43 +08:00
xingyy
d885680be1 refactor(signature): 优化签名面板功能和界面
- 移除了不必要的弹窗和屏幕方向检测功能
- 调整了签名面板的布局和样式
-优化了签名提交流程,直接跳转到支付页面
- 修复了外部链接页面的标题和安全问题
- 改进了支付页面的加载提示
2025-02-26 19:57:21 +08:00
xingyy
c8899348a4 feat(style): 移除 postcss-mobile-forever 插件并调整签名功能
- 移除了 nuxt 配置中的 postcss-mobile-forever 插件- 在 package.json 和 pnpm-lock.yaml 中添加了 postcss-px-to-viewport 依赖
-调整了签名面板的样式,为 VueSignaturePad 添加了高度属性
- 移除了签名确认时的冗余代码,优化了确认流程
2025-02-26 19:03:15 +08:00
70 changed files with 2869 additions and 594 deletions

View File

@ -1,4 +1,4 @@
import { request } from '@/api/http.js'
import { request } from '@/api-collect-code/http.js'
export async function checkPhone(data) {
return await request({
@ -9,7 +9,7 @@ export async function checkPhone(data) {
}
export async function userSend(data) {
return await request( {
url:'/api/v1/m/user/send',
url:'/api/v1/m/user/mobile/send',
method: 'POST',
data
})

View File

@ -1,4 +1,4 @@
import { request } from '@/api/http.js'
import { request } from '@/api-collect-code/http.js'
export async function offlineQrcodeList(data) {
return await request( {
@ -56,7 +56,7 @@ export async function offlineQrcode(data) {
export async function createOrder(data) {
return await request( {
url:'/api/v1/offlineQrcode/createOrder',
url:'/api/v1/offlineQrcode/createOrder/V2',
method: 'POST',
data
})

View File

@ -9,9 +9,9 @@ let http
// HTTP 状态码映射 - 使用i18n国际化
export function setupHttp() {
if (http) return http
const {token}= codeAuthStore()
const {codeToken}= codeAuthStore()
const config = useRuntimeConfig()
const baseURL = config.public.NUXT_PUBLIC_API_COLLECT_CODE
const baseURL = config.public.NUXT_PUBLIC_API_BASE
const router = useRouter()
const i18n = useNuxtApp().$i18n
@ -43,7 +43,7 @@ export function setupHttp() {
// 添加 token
options.headers = {
...options.headers,
Authorization: token.value,
Authorization: codeToken.value,
'accept-language': i18n.locale.value
}
@ -64,11 +64,11 @@ export function setupHttp() {
if (data.status === 1) {
message.error(data.msg || i18n.t('http.error.operationFailed'))
}
console.log('拦截响应',data)
// 处理登录失效
if (data.status === 401) {
message.error(i18n.t('http.error.loginExpired'))
token.value = '' // 清除 token
codeToken.value = '' // 清除 token
router.replace('/collectCode/login')
}

View File

@ -81,7 +81,7 @@ export async function fddCheck(data) {
export async function createBuyOrder(data) {
return await request( {
url:'/api/v1/m/auction/createBuyOrder',
url:'/api/v1/m/auction/createBuyOrder/v2',
method: 'POST',
data
})

View File

@ -1,23 +1,20 @@
<script setup>
import {useI18n} from 'vue-i18n'
import {message} from '@/components/x-message/useMessage.js'
import {hideMinWindow1} from "@/components/floatingBubble/floating.js";
// message.success('success')
import AppSkeleton from '@/components/app-skeleton/index.vue'
const {t} = useI18n()
useHead({
title: useI18n().t('appSetting.appName'),
title: t('appSetting.appName'),
meta: [
{name: 'description', content: useI18n().t('appSetting.appDescription')},
{name: 'keywords', content: useI18n().t('appSetting.appKeyWords')},
{name: 'description', content: t('appSetting.appDescription')},
{name: 'keywords', content: t('appSetting.appKeyWords')},
],
})
//
const router = useRouter()
const route = useRoute()
const slideDirection = ref('slide-left')
const { locale } = useI18n()
// locale.value = 'en-US'
//
const routeHistory = ref([])
@ -40,10 +37,13 @@ if (to.path==='/'){
//
provide('slideDirection', slideDirection)
</script>
<template>
<client-only>
<!-- 骨架屏组件 -->
<VanConfigProvider>
<NuxtLoadingIndicator
color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)"/>

View File

@ -31,7 +31,7 @@ const subTitle = computed(() => {
return route.meta.subTitle ? t(route.meta.subTitle) : ''
})
const showLeftArrow = computed(() => route.name && routeWhiteList.includes(route.name))
console.log('route.name',route.name)
</script>
<template>

View File

@ -0,0 +1,276 @@
<template>
<div class="stripe-container">
<form id="payment-form" @submit.prevent="handleSubmit">
<div id="payment-element">
<!--Stripe.js injects the Payment Element-->
</div>
<button id="submit">
<div class="spinner hidden" id="spinner"></div>
<span id="button-text">Pay now</span>
</button>
<div id="payment-message" class="hidden"></div>
</form>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// The items the customer wants to buy
const items = [{ id: "xl-tshirt", amount: 1000 }]
const stripe = ref(null)
const elements = ref(null)
const isLoading = ref(false)
onMounted(async () => {
try {
// Stripe
stripe.value = window.Stripe("pk_test_51QfbSAAB1Vm8VfJq3AWsR4k2mZjnlF7XFrmlbc6XVXrtwXquAUfwzZmOFDbxMIAwqJBgqao8KLt2wmPc4vNOCTeo00WB78KtfV")
await initialize()
} catch (error) {
showMessage("Failed to initialize payment system")
}
})
// Fetches a payment intent and captures the client secret
const initialize = async () => {
try {
const response = await fetch("/create-payment-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items }),
})
const { clientSecret } = await response.json()
const appearance = {
theme: 'stripe',
}
// elements
elements.value = stripe.value.elements({
appearance,
clientSecret,
})
// Payment Element
const paymentElement = elements.value.create("payment", {
layout: "accordion",
})
// DOM
const mountElement = document.getElementById("payment-element")
if (mountElement) {
paymentElement.mount(mountElement)
} else {
throw new Error("Payment element mount point not found")
}
} catch (error) {
showMessage("Failed to load payment form")
}
}
const handleSubmit = async () => {
if (!stripe.value || !elements.value) {
showMessage("Payment system not initialized")
return
}
setLoading(true)
try {
const { error } = await stripe.value.confirmPayment({
elements: elements.value,
confirmParams: {
return_url: window.location.origin + "/complete.html",
},
})
if (error) {
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message)
} else {
showMessage("An unexpected error occurred.")
}
}
} catch (e) {
showMessage("Payment processing failed")
} finally {
setLoading(false)
}
}
const showMessage = (messageText) => {
const messageContainer = document.querySelector("#payment-message")
messageContainer.classList.remove("hidden")
messageContainer.textContent = messageText
setTimeout(() => {
messageContainer.classList.add("hidden")
messageContainer.textContent = ""
}, 4000)
}
// Show a spinner on payment submission
const setLoading = (isLoading) => {
if (isLoading) {
// Disable the button and show a spinner
document.querySelector("#submit").disabled = true
document.querySelector("#spinner").classList.remove("hidden")
document.querySelector("#button-text").classList.add("hidden")
} else {
document.querySelector("#submit").disabled = false
document.querySelector("#spinner").classList.add("hidden")
document.querySelector("#button-text").classList.remove("hidden")
}
}
</script>
<style scoped>
.stripe-container {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
height: 100vh;
width: 100vw;
}
form {
width: 30vw;
min-width: 500px;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
margin-top: auto;
margin-bottom: auto;
}
.hidden {
display: none;
}
#payment-message {
color: rgb(105, 115, 134);
font-size: 16px;
line-height: 20px;
padding-top: 12px;
text-align: center;
}
#payment-element {
margin-bottom: 24px;
}
button {
background: #0055DE;
font-family: Arial, sans-serif;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: block;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
button:hover {
filter: contrast(115%);
}
button:disabled {
opacity: 0.5;
cursor: default;
}
.spinner,
.spinner:before,
.spinner:after {
border-radius: 50%;
}
.spinner {
color: #ffffff;
font-size: 22px;
text-indent: -99999px;
margin: 0px auto;
position: relative;
width: 20px;
height: 20px;
box-shadow: inset 0 0 0 2px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before,
.spinner:after {
position: absolute;
content: "";
}
.spinner:before {
width: 10.4px;
height: 20.4px;
background: #0055DE;
border-radius: 20.4px 0 0 20.4px;
top: -0.2px;
left: -0.2px;
-webkit-transform-origin: 10.4px 10.2px;
transform-origin: 10.4px 10.2px;
-webkit-animation: loading 2s infinite ease 1.5s;
animation: loading 2s infinite ease 1.5s;
}
.spinner:after {
width: 10.4px;
height: 10.2px;
background: #0055DE;
border-radius: 0 10.2px 10.2px 0;
top: -0.1px;
left: 10.2px;
-webkit-transform-origin: 0px 10.2px;
transform-origin: 0px 10.2px;
-webkit-animation: loading 2s infinite ease;
animation: loading 2s infinite ease;
}
@-webkit-keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@media only screen and (max-width: 600px) {
form {
width: 80vw;
min-width: initial;
}
}
</style>

View File

@ -0,0 +1,194 @@
<script setup>
//
</script>
<template>
<transition name="fade">
<div class="app-skeleton">
<!-- 顶部导航栏骨架 -->
<div class="skeleton-header">
<div class="skeleton-avatar"></div>
<div class="skeleton-title"></div>
</div>
<!-- 内容区域骨架 -->
<div class="skeleton-content">
<!-- 轮播图骨架 -->
<div class="skeleton-banner"></div>
<!-- 菜单项骨架 -->
<div class="skeleton-menu">
<div class="skeleton-menu-item" v-for="i in 4" :key="i"></div>
</div>
<!-- 列表项骨架 -->
<div class="skeleton-list">
<div class="skeleton-list-item" v-for="i in 3" :key="i">
<div class="skeleton-list-image"></div>
<div class="skeleton-list-content">
<div class="skeleton-list-title"></div>
<div class="skeleton-list-desc"></div>
<div class="skeleton-list-price"></div>
</div>
</div>
</div>
</div>
<!-- 底部导航栏骨架 -->
<div class="skeleton-tabbar">
<div class="skeleton-tab-item" v-for="i in 4" :key="i"></div>
</div>
</div>
</transition>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.app-skeleton {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
z-index: 9999;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 骨架屏动画 */
@keyframes skeleton-loading {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
.skeleton-header, .skeleton-avatar, .skeleton-title, .skeleton-banner,
.skeleton-menu-item, .skeleton-list-image, .skeleton-list-title,
.skeleton-list-desc, .skeleton-list-price, .skeleton-tab-item {
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
background-size: 400% 100%;
animation: skeleton-loading 1.4s ease infinite;
}
/* 顶部导航栏 */
.skeleton-header {
height: 44px;
padding: 0 16px;
display: flex;
align-items: center;
border-bottom: 1px solid #f5f5f5;
}
.skeleton-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
}
.skeleton-title {
width: 120px;
height: 16px;
margin-left: 12px;
border-radius: 4px;
}
/* 内容区域 */
.skeleton-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.skeleton-banner {
width: 100%;
height: 150px;
border-radius: 8px;
margin-bottom: 20px;
}
/* 菜单项 */
.skeleton-menu {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.skeleton-menu-item {
width: 60px;
height: 60px;
border-radius: 8px;
}
/* 列表项 */
.skeleton-list-item {
display: flex;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #f5f5f5;
}
.skeleton-list-image {
width: 80px;
height: 80px;
border-radius: 4px;
flex-shrink: 0;
}
.skeleton-list-content {
flex: 1;
margin-left: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.skeleton-list-title {
height: 16px;
width: 80%;
border-radius: 4px;
margin-bottom: 8px;
}
.skeleton-list-desc {
height: 12px;
width: 60%;
border-radius: 4px;
margin-bottom: 8px;
}
.skeleton-list-price {
height: 16px;
width: 40%;
border-radius: 4px;
}
/* 底部导航栏 */
.skeleton-tabbar {
height: 50px;
display: flex;
justify-content: space-around;
align-items: center;
border-top: 1px solid #f5f5f5;
padding: 0 16px;
}
.skeleton-tab-item {
width: 24px;
height: 24px;
border-radius: 4px;
}
</style>

View File

@ -4,6 +4,7 @@
*/
import { createApp } from 'vue'
import MinWindow from '@/components/floatingBubble/index.vue'
import { createI18n } from 'vue-i18n'
// 全局单例状态管理
let minWindowInstance = null // 组件实例引用
@ -17,7 +18,6 @@ let container = null // DOM容器元素
*/
export const showMinWindow1 = (props = {}) => {
// 服务端渲染时直接返回
console.log('!process.client',!process.client)
if (!process.client) return null
// 如果实例已存在,避免重复创建
@ -39,12 +39,26 @@ export const showMinWindow1 = (props = {}) => {
// 创建Vue应用实例
const app = createApp(MinWindow, props)
// 获取当前 Nuxt 应用的 i18n 配置
const nuxtApp = window?.__nuxt
const i18nConfig = nuxtApp?.$i18n?.options || {
legacy: false,
locale: 'en',
messages: {}
}
// 为独立组件创建 i18n 实例
const i18n = createI18n(i18nConfig)
// 安装 i18n
app.use(i18n)
minWindowApp = app
minWindowInstance = app.mount(container)
return minWindowInstance
} catch (error) {
console.error('创建浮动气泡时发生错误:', error)
// 发生错误时确保清理资源
hideMinWindow1()
return null
@ -56,7 +70,7 @@ export const showMinWindow1 = (props = {}) => {
* 清理所有相关资源和DOM元素
*/
export const hideMinWindow1 = () => {
console.log('!minWindowApp && !container', !minWindowApp && !container);
if (!minWindowApp && !container) return
@ -77,8 +91,7 @@ export const hideMinWindow1 = () => {
document.body.removeChild(existingContainer)
}
} catch (error) {
console.error('清理浮动气泡时发生错误:', error)
} finally {
} finally {
// 重置所有状态
minWindowApp = null
minWindowInstance = null

View File

@ -6,9 +6,12 @@
import { watch, onUnmounted } from 'vue'
import { hideMinWindow1 } from './floating'
const { t } = useI18n()
// nuxt/app useNuxtApp
const { $i18n } = useNuxtApp()
// useI18n
const t = (key) => $i18n.t(key)
//
const props = defineProps({
/** 点击气泡时的回调函数 */
onClick: {

View File

@ -28,7 +28,6 @@ export const showMinWindow = (snapshot, props = {}) => {
})
app.config.errorHandler = (err) => {
console.error('MinWindow Error:', err)
hideMinWindow()
}

View File

@ -0,0 +1,331 @@
<script setup>
import { onMounted, ref } from 'vue'
const stripe = Stripe("pk_test_51QfbSAAB1Vm8VfJq3AWsR4k2mZjnlF7XFrmlbc6XVXrtwXquAUfwzZmOFDbxMIAwqJBgqao8KLt2wmPc4vNOCTeo00WB78KtfV")
const items = [{ id: "xl-tshirt", amount: 1000 }]
const elements = ref(null)
const paymentMessage = ref('')
const isLoading = ref(false)
const showSpinner = ref(false)
async function initialize() {
const clientSecret = 'pi_3QxII1AB1Vm8VfJq1OyR3bkz_secret_d8fgL53X6T3MQpYfi2lRH3V1F'
const appearance = {
theme: 'stripe',
}
elements.value = stripe.elements({ appearance, clientSecret })
const paymentElementOptions = {
layout: "accordion",
}
const paymentElement = elements.value.create("payment", paymentElementOptions)
paymentElement.mount("#payment-element")
}
async function handleSubmit(e) {
e.preventDefault()
setLoading(true)
const { error } = await stripe.confirmPayment({
elements: elements.value,
confirmParams: {
return_url: "http://localhost:4242/complete",
},
})
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message)
} else {
showMessage("An unexpected error occurred.")
}
setLoading(false)
}
function showMessage(messageText) {
paymentMessage.value = messageText
setTimeout(() => {
paymentMessage.value = ''
}, 4000)
}
function setLoading(loading) {
isLoading.value = loading
showSpinner.value = loading
}
onMounted(() => {
initialize()
})
</script>
<template>
<form id="payment-form" @submit="handleSubmit">
<div id="payment-element">
</div>
<button id="submit">
<div class="spinner" :class="{ hidden: !showSpinner }" id="spinner"></div>
<span id="button-text" :class="{ hidden: showSpinner }">Pay now</span>
</button>
<div id="payment-message" :class="{ hidden: !paymentMessage }">{{ paymentMessage }}</div>
</form>
</template>
<style scoped>
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
height: 100vh;
width: 100vw;
}
form {
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
margin-top: auto;
margin-bottom: auto;
}
.hidden {
display: none;
}
#payment-message {
color: rgb(105, 115, 134);
font-size: 16px;
line-height: 20px;
padding-top: 12px;
text-align: center;
}
#payment-element {
margin-bottom: 24px;
}
/* Buttons and links */
button {
background: #0055DE;
font-family: Arial, sans-serif;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: block;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
button:hover {
filter: contrast(115%);
}
button:disabled {
opacity: 0.5;
cursor: default;
}
/* spinner/processing state, errors */
.spinner,
.spinner:before,
.spinner:after {
border-radius: 50%;
}
.spinner {
color: #ffffff;
font-size: 22px;
text-indent: -99999px;
margin: 0px auto;
position: relative;
width: 20px;
height: 20px;
box-shadow: inset 0 0 0 2px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before,
.spinner:after {
position: absolute;
content: "";
}
.spinner:before {
width: 10.4px;
height: 20.4px;
background: #0055DE;
border-radius: 20.4px 0 0 20.4px;
top: -0.2px;
left: -0.2px;
-webkit-transform-origin: 10.4px 10.2px;
transform-origin: 10.4px 10.2px;
-webkit-animation: loading 2s infinite ease 1.5s;
animation: loading 2s infinite ease 1.5s;
}
.spinner:after {
width: 10.4px;
height: 10.2px;
background: #0055DE;
border-radius: 0 10.2px 10.2px 0;
top: -0.1px;
left: 10.2px;
-webkit-transform-origin: 0px 10.2px;
transform-origin: 0px 10.2px;
-webkit-animation: loading 2s infinite ease;
animation: loading 2s infinite ease;
}
/* Payment status page */
#payment-status {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
row-gap: 30px;
width: 30vw;
min-width: 500px;
min-height: 380px;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
opacity: 0;
animation: fadeInAnimation 1s ease forwards;
}
#status-icon {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
width: 40px;
border-radius: 50%;
}
h2 {
margin: 0;
color: #30313D;
text-align: center;
}
a {
text-decoration: none;
font-size: 16px;
font-weight: 600;
font-family: Arial, sans-serif;
display: block;
}
a:hover {
filter: contrast(120%);
}
#details-table {
overflow-x: auto;
width: 100%;
}
table {
width: 100%;
font-size: 14px;
border-collapse: collapse;
}
table tbody tr:first-child td {
border-top: 1px solid #E6E6E6; /* Top border */
padding-top: 10px;
}
table tbody tr:last-child td {
border-bottom: 1px solid #E6E6E6; /* Bottom border */
}
td {
padding-bottom: 10px;
}
.TableContent {
text-align: right;
color: #6D6E78;
}
.TableLabel {
font-weight: 600;
color: #30313D;
}
#view-details {
color: #0055DE;
}
#retry-button {
text-align: center;
background: #0055DE;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
@-webkit-keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes fadeInAnimation {
to {
opacity: 1;
}
}
@media only screen and (max-width: 600px) {
form, #payment-status{
width: 80vw;
min-width: initial;
}
}
.hidden {
display: none;
}
form {
width:100vw;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
margin-top: auto;
margin-bottom: auto;
}
/* 其他样式保持不变... */
</style>

View File

@ -0,0 +1,115 @@
<script setup>
import { onMounted, ref } from 'vue'
const stripe = Stripe("pk_test_51QfbSAAB1Vm8VfJq3AWsR4k2mZjnlF7XFrmlbc6XVXrtwXquAUfwzZmOFDbxMIAwqJBgqao8KLt2wmPc4vNOCTeo00WB78KtfV")
const statusIcon = ref('')
const statusText = ref('')
const intentId = ref('')
const intentStatus = ref('')
const viewDetailsUrl = ref('')
const iconColor = ref('')
const SuccessIcon = `<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4695 0.232963C15.8241 0.561287 15.8454 1.1149 15.5171 1.46949L6.14206 11.5945C5.97228 11.7778 5.73221 11.8799 5.48237 11.8748C5.23253 11.8698 4.99677 11.7582 4.83452 11.5681L0.459523 6.44311C0.145767 6.07557 0.18937 5.52327 0.556912 5.20951C0.924454 4.89575 1.47676 4.93936 1.79051 5.3069L5.52658 9.68343L14.233 0.280522C14.5613 -0.0740672 15.1149 -0.0953599 15.4695 0.232963Z" fill="white"/>
</svg>`
const ErrorIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.25628 1.25628C1.59799 0.914573 2.15201 0.914573 2.49372 1.25628L8 6.76256L13.5063 1.25628C13.848 0.914573 14.402 0.914573 14.7437 1.25628C15.0854 1.59799 15.0854 2.15201 14.7437 2.49372L9.23744 8L14.7437 13.5063C15.0854 13.848 15.0854 14.402 14.7437 14.7437C14.402 15.0854 13.848 15.0854 13.5063 14.7437L8 9.23744L2.49372 14.7437C2.15201 15.0854 1.59799 15.0854 1.25628 14.7437C0.914573 14.402 0.914573 13.848 1.25628 13.5063L6.76256 8L1.25628 2.49372C0.914573 2.15201 0.914573 1.59799 1.25628 1.25628Z" fill="white"/>
</svg>`
const InfoIcon = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 1.5H4C2.61929 1.5 1.5 2.61929 1.5 4V10C1.5 11.3807 2.61929 12.5 4 12.5H10C11.3807 12.5 12.5 11.3807 12.5 10V4C12.5 2.61929 11.3807 1.5 10 1.5ZM4 0C1.79086 0 0 1.79086 0 4V10C0 12.2091 1.79086 14 4 14H10C12.2091 14 14 12.2091 14 10V4C14 1.79086 12.2091 0 10 0H4Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 7C5.25 6.58579 5.58579 6.25 6 6.25H7.25C7.66421 6.25 8 6.58579 8 7V10.5C8 10.9142 7.66421 11.25 7.25 11.25C6.83579 11.25 6.5 10.9142 6.5 10.5V7.75H6C5.58579 7.75 5.25 7.41421 5.25 7Z" fill="white"/>
<path d="M5.75 4C5.75 3.31075 6.31075 2.75 7 2.75C7.68925 2.75 8.25 3.31075 8.25 4C8.25 4.68925 7.68925 5.25 7 5.25C6.31075 5.25 5.75 4.68925 5.75 4Z" fill="white"/>
</svg>`
function setPaymentDetails(intent) {
let status = "Something went wrong, please try again."
let color = "#DF1B41"
let icon = ErrorIcon
if (!intent) {
setErrorState()
return
}
switch (intent.status) {
case "succeeded":
status = "Payment succeeded"
color = "#30B130"
icon = SuccessIcon
break
case "processing":
status = "Your payment is processing."
color = "#6D6E78"
icon = InfoIcon
break
case "requires_payment_method":
status = "Your payment was not successful, please try again."
break
}
iconColor.value = color
statusIcon.value = icon
statusText.value = status
intentId.value = intent.id
intentStatus.value = intent.status
viewDetailsUrl.value = `https://dashboard.stripe.com/payments/${intent.id}`
}
function setErrorState() {
iconColor.value = "#DF1B41"
statusIcon.value = ErrorIcon
statusText.value = "Something went wrong, please try again."
}
async function checkStatus() {
const urlParams = new URLSearchParams(window.location.search)
const clientSecret = urlParams.get("payment_intent_client_secret")
if (!clientSecret) {
setErrorState()
return
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret)
setPaymentDetails(paymentIntent)
}
onMounted(() => {
checkStatus()
})
</script>
<template>
<div id="payment-status">
<div id="status-icon" :style="{ backgroundColor: iconColor }" v-html="statusIcon"></div>
<h2 id="status-text">{{ statusText }}</h2>
<div id="details-table">
<table>
<tbody>
<tr>
<td class="TableLabel">id</td>
<td id="intent-id" class="TableContent">{{ intentId }}</td>
</tr>
<tr>
<td class="TableLabel">status</td>
<td id="intent-status" class="TableContent">{{ intentStatus }}</td>
</tr>
</tbody>
</table>
</div>
<a :href="viewDetailsUrl" id="view-details" rel="noopener noreferrer" target="_blank">
View details
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 3.49998C2.64175 3.49998 2.25 3.89173 2.25 4.37498V11.375C2.25 11.8582 2.64175 12.25 3.125 12.25H10.125C10.6082 12.25 11 11.8582 11 11.375V9.62498C11 9.14173 11.3918 8.74998 11.875 8.74998C12.3582 8.74998 12.75 9.14173 12.75 9.62498V11.375C12.75 12.8247 11.5747 14 10.125 14H3.125C1.67525 14 0.5 12.8247 0.5 11.375V4.37498C0.5 2.92524 1.67525 1.74998 3.125 1.74998H4.875C5.35825 1.74998 5.75 2.14173 5.75 2.62498C5.75 3.10823 5.35825 3.49998 4.875 3.49998H3.125Z" fill="#0055DE"/>
<path d="M8.66672 0C8.18347 0 7.79172 0.391751 7.79172 0.875C7.79172 1.35825 8.18347 1.75 8.66672 1.75H11.5126L4.83967 8.42295C4.49796 8.76466 4.49796 9.31868 4.83967 9.66039C5.18138 10.0021 5.7354 10.0021 6.07711 9.66039L12.7501 2.98744V5.83333C12.7501 6.31658 13.1418 6.70833 13.6251 6.70833C14.1083 6.70833 14.5001 6.31658 14.5001 5.83333V0.875C14.5001 0.391751 14.1083 0 13.6251 0H8.66672Z" fill="#0055DE"/>
</svg>
</a>
<NuxtLink id="retry-button" to="/checkout">Test another</NuxtLink>
</div>
</template>
<style scoped>
/* 将原来 checkout.css 中的样式复制到这里 */
</style>

View File

@ -1,5 +1,7 @@
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const {t} =useI18n()
const props = defineProps({
modelValue: {
type: [Number, String]
@ -21,7 +23,7 @@ const props = defineProps({
placeholder: {
type: String,
default: '请选择'
default: () => useI18n().t('components.form.pleaseSelect')
},
disabled: {
@ -75,9 +77,9 @@ const openPopup=()=>{
@confirm="onConfirm"
@cancel="show = false"
:default-index="columns.findIndex(x => x.value === modelValue)"
title="请选择"
confirm-button-text="确定"
cancel-button-text="取消"
:title="t('components.form.pleaseSelect')"
:confirm-button-text="t('components.dialog.confirm')"
:cancel-button-text="t('components.dialog.cancel')"
/>
</van-popup>
</div>

View File

@ -15,8 +15,7 @@ const initData = async () => {
const res = await userArtwork({uuid})
if (res.status === 0) {
detail.value = res.data
console.log('detail',detail.value)
}
}
}
const router = useRouter();
const position = ref({x: window?.innerWidth - 120 || 0, y: 240})
@ -59,8 +58,7 @@ const goPay=()=>{
}else if (detail.value.status===4){
router.push('/payment')
}
console.log('detail',detail.value)
//router.push('/payment')
//router.push('/payment')
}
onMounted(() => {
document.addEventListener('mousemove', onDrag)

View File

@ -0,0 +1,402 @@
<script setup>
import { onMounted, ref } from 'vue'
import {authStore} from "~/stores/auth/index.js";
import {orderQuery} from "~/api/goods/index.js";
import { WebSocketClient } from '@/utils/websocket'
const config = useRuntimeConfig()
definePageMeta({
layout: 'default',
title: 'Stripe支付'
})
const stripe = Stripe(config.public.NUXT_PUBLIC_PKEY)
const route = useRoute()
const baseURL = config.public.NUXT_PUBLIC_API_BASE
const items = [{ id: "xl-tshirt", amount: 1000 }]
const elements = ref(null)
const paymentMessage = ref('')
const isLoading = ref(false)
const showSpinner = ref(false)
let pollTimer = null
let timeoutTimer = null
const router = useRouter()
const startPolling = () => {
pollTimer = setInterval(async () => {
const res = await orderQuery({
orderNo: route.query.payUid
})
if (res.status === 0) {
if (res.data.status !== 3) {
clearInterval(pollTimer)
clearTimeout(timeoutTimer)
router.replace({
path: route.query.returnUrl,
query: {
orderNo: route.query.payUid
}
})
}
}
}, 1000)
/* timeoutTimer = setTimeout(() => {
clearInterval(pollTimer)
setLoading(false)
}, 180000)*/
}
let wsClient=null
const watchWebSocket = () => {
wsClient = new WebSocketClient(
config.public.NUXT_PUBLIC_SOCKET_URL
)
const ws = wsClient.connect('/api/v1/order/ws/v2', {
PayUid: route.query.payUid,
})
ws.onOpen(() => {
})
ws.onMessage((event) => {
router.replace({
path: route.query.returnUrl,
query: {
orderNo: route.query.payUid
}
})
})
ws.onClose(() => {
})
}
async function initialize() {
const clientSecret = route.query.stripeKey
const appearance = {
theme: 'stripe',
}
elements.value = stripe.elements({ appearance, clientSecret })
const paymentElementOptions = {
layout: "accordion",
}
const paymentElement = elements.value.create("payment", paymentElementOptions)
paymentElement.mount("#payment-element")
}
async function handleSubmit(e) {
e.preventDefault()
setLoading(true)
const { error } = await stripe.confirmPayment({
elements: elements.value,
confirmParams: {
return_url: `${baseURL}${route.query.returnUrl}?orderNo=${route.query.payUid}`,
},
})
if (error) {
/* clearInterval(pollTimer)
clearTimeout(timeoutTimer)*/
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message)
} else {
showMessage("An unexpected error occurred.")
}
setLoading(false)
}
}
function showMessage(messageText) {
paymentMessage.value = messageText
setTimeout(() => {
paymentMessage.value = ''
}, 4000)
}
function setLoading(loading) {
isLoading.value = loading
showSpinner.value = loading
}
onUnmounted(()=>{
wsClient.disconnect()
clearTimeout(timeoutTimer)
clearInterval(pollTimer)
})
onMounted(() => {
watchWebSocket()
initialize()
startPolling()
})
</script>
<template>
<form id="payment-form" @submit="handleSubmit">
<div id="payment-element">
</div>
<button id="submit">
<div class="spinner" :class="{ hidden: !showSpinner }" id="spinner"></div>
<span id="button-text" :class="{ hidden: showSpinner }">Pay now</span>
</button>
<div id="payment-message" :class="{ hidden: !paymentMessage }">{{ paymentMessage }}</div>
</form>
</template>
<style scoped>
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
height: 100vh;
width: 100vw;
}
form {
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
margin-top: auto;
margin-bottom: auto;
}
.hidden {
display: none;
}
#payment-message {
color: rgb(105, 115, 134);
font-size: 16px;
line-height: 20px;
padding-top: 12px;
text-align: center;
}
#payment-element {
margin-bottom: 24px;
}
/* Buttons and links */
button {
background: #0055DE;
font-family: Arial, sans-serif;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: block;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
button:hover {
filter: contrast(115%);
}
button:disabled {
opacity: 0.5;
cursor: default;
}
/* spinner/processing state, errors */
.spinner,
.spinner:before,
.spinner:after {
border-radius: 50%;
}
.spinner {
color: #ffffff;
font-size: 22px;
text-indent: -99999px;
margin: 0px auto;
position: relative;
width: 20px;
height: 20px;
box-shadow: inset 0 0 0 2px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before,
.spinner:after {
position: absolute;
content: "";
}
.spinner:before {
width: 10.4px;
height: 20.4px;
background: #0055DE;
border-radius: 20.4px 0 0 20.4px;
top: -0.2px;
left: -0.2px;
-webkit-transform-origin: 10.4px 10.2px;
transform-origin: 10.4px 10.2px;
-webkit-animation: loading 2s infinite ease 1.5s;
animation: loading 2s infinite ease 1.5s;
}
.spinner:after {
width: 10.4px;
height: 10.2px;
background: #0055DE;
border-radius: 0 10.2px 10.2px 0;
top: -0.1px;
left: 10.2px;
-webkit-transform-origin: 0px 10.2px;
transform-origin: 0px 10.2px;
-webkit-animation: loading 2s infinite ease;
animation: loading 2s infinite ease;
}
/* Payment status page */
#payment-status {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
row-gap: 30px;
width: 30vw;
min-width: 500px;
min-height: 380px;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
opacity: 0;
animation: fadeInAnimation 1s ease forwards;
}
#status-icon {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
width: 40px;
border-radius: 50%;
}
h2 {
margin: 0;
color: #30313D;
text-align: center;
}
a {
text-decoration: none;
font-size: 16px;
font-weight: 600;
font-family: Arial, sans-serif;
display: block;
}
a:hover {
filter: contrast(120%);
}
#details-table {
overflow-x: auto;
width: 100%;
}
table {
width: 100%;
font-size: 14px;
border-collapse: collapse;
}
table tbody tr:first-child td {
border-top: 1px solid #E6E6E6; /* Top border */
padding-top: 10px;
}
table tbody tr:last-child td {
border-bottom: 1px solid #E6E6E6; /* Bottom border */
}
td {
padding-bottom: 10px;
}
.TableContent {
text-align: right;
color: #6D6E78;
}
.TableLabel {
font-weight: 600;
color: #30313D;
}
#view-details {
color: #0055DE;
}
#retry-button {
text-align: center;
background: #0055DE;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
@-webkit-keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes fadeInAnimation {
to {
opacity: 1;
}
}
@media only screen and (max-width: 600px) {
form, #payment-status{
width: 80vw;
min-width: initial;
}
}
.hidden {
display: none;
}
form {
width:100vw;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
margin-top: auto;
margin-bottom: auto;
}
/* 其他样式保持不变... */
</style>

View File

@ -8,7 +8,7 @@ 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 {userInfo, codeToken, fingerprint} = codeAuthStore()
const router = useRouter();
const route = useRoute();
const {locale} = useI18n()
@ -33,10 +33,9 @@ const startCountdown = () => {
}, 1000);
}
const countdown = ref(0);
const phoneNum = ref('17630920520')
const code = ref('123789')
const phoneNum = ref('')
const code = ref('')
const pane = ref(0)
const showKeyboard = ref(false);
const getFingerprint = async () => {
const fp = await FingerprintJS.load()
const result = await fp.get()
@ -50,36 +49,29 @@ const checkFingerprint = async () => {
await router.push('/collectCode/mine')
}
}
const codeInput = ref(null)
const isFocused = ref(false)
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'})
try {
const res = await checkPhone({
tel: phoneNum.value,
})
if (res.status === 0) {
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value = true
const sendRes = await userSend({telNum: phoneNum.value, zone: '+86'})
startCountdown()
pane.value = 1
await nextTick()
vanSwipeRef.value?.swipeTo(pane.value)
}
} catch (error) {
console.error('获取验证码失败:', error)
} finally {
loadingRef.value.loading1 = false
}
/* 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
@ -98,7 +90,7 @@ const goLogin = async () => {
})
if (res.status === 0) {
userInfo.value = res.data.accountInfo
token.value = res.data.token
codeToken.value = res.data.token
fingerprint.value = await getFingerprint()
await router.push('/collectCode/mine');
@ -114,98 +106,124 @@ const togglePasswordVisibility = () => {
</script>
<template>
<div class="h-[100vh] w-[100vw] bg-[url('@/static/images/asdfsdd.png')] bg-cover px-[31px] pt-[86px]">
<div class="grow-1 w-[100vw] bg-[url('@/static/images/asdfsdd.png')] bg-bottom 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="$t('collectCode.login.phoneNumberPlaceholder')">
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
{{ $t('collectCode.login.phoneNumber') }}
</div>
</template>
</van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]" v-show="loginType === 1">
<van-field
v-model="password"
:type="showPassword ? 'text' : 'password'"
clearable
:placeholder="$t('collectCode.login.passwordPlaceholder')"
>
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
{{ $t('collectCode.login.password') }}
</div>
</template>
<template #button>
<div class="flex justify-center items-center">
<van-icon
size="20"
:name="showPassword ? 'eye-o' : 'closed-eye'"
@click="togglePasswordVisibility"
/>
</div>
</template>
</van-field>
</div>
<div class="flex justify-end mt-[10px]">
<div class="text-[14px] text-[#2B53AC]" @click="changeToPwd">
{{ loginType === 0 ? $t('collectCode.login.passwordLogin') : $t('collectCode.login.codeLogin') }}
<div v-if="pane === 0">
<div>
<div class="">
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="phoneNum" clearable :placeholder="$t('collectCode.login.phoneNumberPlaceholder')">
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
{{ $t('collectCode.login.phoneNumber') }}
</div>
</template>
</van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]" v-if="loginType === 1">
<van-field
v-model="password"
:type="showPassword ? 'text' : 'password'"
clearable
:placeholder="$t('collectCode.login.passwordPlaceholder')"
>
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
{{ $t('collectCode.login.password') }}
</div>
</template>
<template #button>
<div class="flex justify-center items-center">
<van-icon
size="20"
:name="showPassword ? 'eye-o' : 'closed-eye'"
@click="togglePasswordVisibility"
/>
</div>
</template>
</van-field>
</div>
<div class="flex justify-end mt-[10px]">
<div class="text-[14px] text-[#2B53AC]" @click="changeToPwd">
{{ loginType === 0 ? $t('collectCode.login.passwordLogin') : $t('collectCode.login.codeLogin') }}
</div>
</div>
</div>
<div/>
</div>
<div class="mt-[55px]">
<div v-if="loginType === 0">
<van-button :loading="loadingRef.loading1" v-if="phoneNum" :loading-text="$t('collectCode.login.getCode')"
type="primary" block style="height: 48px" @click="getCode">{{ $t('collectCode.login.getCode') }}
</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">{{ $t('collectCode.login.getCode') }}</van-button>
</div>
<div v-else>
<van-button type="primary" v-if="password" block :loading="loadingRef.loading2" :loading-text="$t('collectCode.login.login')"
style="height: 48px;margin-top:10px" @click="goLogin">{{ $t('collectCode.login.login') }}
</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">{{ $t('collectCode.login.login') }}</van-button>
<div class="mt-[55px]">
<div v-if="loginType === 0">
<van-button :loading="loadingRef.loading1" v-if="phoneNum" :loading-text="$t('collectCode.login.getCode')"
type="primary" block style="height: 48px" @click="getCode">{{
$t('collectCode.login.getCode')
}}
</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">
{{ $t('collectCode.login.getCode') }}
</van-button>
</div>
<div v-else>
<van-button type="primary" v-if="password" block :loading="loadingRef.loading2"
:loading-text="$t('collectCode.login.login')"
style="height: 48px;margin-top:10px" @click="goLogin">{{ $t('collectCode.login.login') }}
</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">
{{ $t('collectCode.login.login') }}
</van-button>
</div>
</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('collectCode.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="flex justify-between">
<div :class="`${countdown>0?'text-#BDBDBD':'text-#2B53AC'} text-14px`">
{{ $t('collectCode.login.reSend') }}<span v-if="countdown>0">({{ countdown }})</span>
<div v-if="pane == 1">
<div>
<div class="flex mb-[16px]">
<div class="text-[16px] text-[#BDBDBD] mr-[10px]">{{ $t('collectCode.login.hasSendTo') }}</div>
<div class="text-[16px] text-[#000]">+86 {{ phoneNum }}</div>
</div>
<div @click="goBack" class="text-#2B53AC text-14px">
{{ $t('collectCode.login.back') }}
<div class="relative">
<van-password-input
:value="code"
:gutter="10"
:mask="false"
:focused="isFocused"
/>
<input
v-model="code"
type="tel"
maxlength="6"
ref="codeInput"
class="opacity-0 absolute top-0 left-0 h-full w-full z-999"
@input="code = $event.target.value.replace(/\D/g, '').slice(0, 6)"
@focus="isFocused = true"
@blur="isFocused = false"
/>
</div>
<div class="flex justify-between">
<div :class="`${countdown>0?'text-#BDBDBD':'text-#2B53AC'} text-14px`">
{{ $t('collectCode.login.reSend') }}<span v-if="countdown>0">({{ countdown }})</span>
</div>
<div @click="goBack" class="text-#2B53AC text-14px">
{{ $t('collectCode.login.back') }}
</div>
</div>
<div class="mt-[17px]">
<van-button v-if="code.length === 6" type="primary" block :loading="loadingRef.loading2"
:loading-text="$t('collectCode.login.login')" style="height: 48px" @click="goLogin">
{{ $t('collectCode.login.login') }}
</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">
{{ $t('collectCode.login.login') }}
</van-button>
</div>
</div>
<div class="mt-[17px]">
<van-button v-if="code.length === 6" type="primary" block :loading="loadingRef.loading2"
:loading-text="$t('collectCode.login.login')" style="height: 48px" @click="goLogin">
{{ $t('collectCode.login.login') }}
</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">
{{ $t('collectCode.login.login') }}
</van-button>
</div>
</div>
</van-swipe-item>
</van-swipe>
<van-number-keyboard v-model="code" :show="showKeyboard" @blur="showKeyboard = false"/>
</div>
</template>

View File

@ -6,7 +6,7 @@ import QRCode from 'qrcode'
import { showImagePreview } from 'vant';
import { useI18n } from 'vue-i18n';
const t = useI18n();
const t = useI18n().t;
const statusLabel=[
{label: t('collectCode.qrcode.status.paid'), value:2, color:'#18A058'},
@ -33,7 +33,6 @@ const getQRBase64 = async () => {
errorCorrectionLevel: 'H'
})
} catch (err) {
console.error('生成二维码失败:', err)
return null
}
}
@ -45,7 +44,26 @@ const openQrCode=async ()=>{
QRUrl.value=base64
show.value=true
}
/**
* 将数字格式化为"250XX"格式其中XX是两位数
* @param {number} num - 要格式化的数字
* @return {string} - 格式化后的字符串
*/
function formatNumber(num) {
//
if (typeof num !== 'number' && isNaN(Number(num))) {
throw new Error('输入必须是有效数字');
}
// ()
const number = Number(num);
// 0
const formattedNum = number.toString().padStart(2, '0');
// "250"
return `250${formattedNum}`;
}
</script>
<template>
@ -59,7 +77,7 @@ const openQrCode=async ()=>{
<XImage class="w-57px h-56px rounded-4px" :src="data.hdPic"></XImage>
</div>
<div class="text-12px text-#1E1E1E">
<div>{{ $t('collectCode.qrcode.card.lotNo') }}{{ data.lotNo }}</div>
<div>{{ $t('collectCode.qrcode.card.lotNo') }}{{ formatNumber(data.lotNo) }}</div>
<div>{{ $t('collectCode.qrcode.card.creator') }}{{ data.userName }}</div>
<div>{{ $t('collectCode.qrcode.card.createTime') }}{{data.createdAt}}</div>
</div>

View File

@ -22,7 +22,7 @@ const localState = ref({
showDetail: false,
showHeight: ''
})
const {t} =useI18n()
const { userInfo, } = codeAuthStore()
const {getOfflineQrcodeList,itemList, loading: storeLoading,pageRef}= goodStore()
const initData = async () => {
@ -30,7 +30,6 @@ const initData = async () => {
}
const show=ref(false)
const close=()=>{
console.log('show',show.value)
show.value=false
}
const logOut=()=>{
@ -49,7 +48,15 @@ const confirm=async ()=>{
message.warning(t('collectCode.message.lotNoRequired'))
return false
}
const res=await offlineQrcodeCreate({...createForm.value,price:String(createForm.value.price)})
function is25Format(num) {
return /^25\d{3}$/.test(String(num));
}
if (!is25Format(createForm.value.lotNo)){
message.warning(t('collectCode.message.lotNoType'))
return
}
const res=await offlineQrcodeCreate({...createForm.value,price:String(createForm.value.price),lotNo:createForm.value.lotNo-25000})
if (res.status===0){
show.value=false
onRefresh()
@ -73,8 +80,11 @@ const loadMore = async () => {
const abnormal=ref(false)
const abnormalRow=ref({})
const inputLotNo=async (data)=>{
if (createForm.value.lotNo<=25000){
return
}
const res=await offlineQrcodeList({
lotNo:createForm.value.lotNo
lotNo:createForm.value.lotNo-25000
})
if (res.status===0){
if (res.data.Data?.length>0){

View File

@ -10,7 +10,7 @@ import {codeAuthStore} from "~/stores-collect-code/auth/index.js";
import {useI18n} from "vue-i18n";
const {t} = useI18n();
const {checkoutSessionUrl,qrUid,qrData} = codeAuthStore()
const {checkoutSessionUrl,qrUid,qrData,codePKey,codePayUid} = codeAuthStore()
const payStatus = ref(0)
definePageMeta({
i18n: 'payment.title'
@ -19,6 +19,7 @@ const changePayStatus = () => {
payStatus.value = payStatus.value === 0 ? 1 : 0
}
const amount = ref('')
const router = useRouter()
const confirmPay = async () => {
if (payStatus.value === 1 && !amount.value) {
message.warning(t('collectCode.payment.enterAmount'))
@ -40,7 +41,18 @@ const confirmPay = async () => {
testReturnEndPoint: '/collectCode/payment/result'
})
if (res.status === 0) {
window.location.href = res.data.checkoutSessionUrl
codePKey.value=res.data.checkoutSessionUrl
codePayUid.value=res.data.payUid
router.push({
path:'/checkoutPage',
query:{
payUid:res.data.payUid,
returnUrl:'/collectCode/payment/result',
stripeKey:res.data.checkoutSessionUrl
}
})
}
}
@ -75,8 +87,8 @@ const handleInput = (e) => {
{{ qrData?.leftPrice }}
</div>
<div class="mb-12px" v-else>
<input v-model="amount" class="w-272px h-48px bg-#F3F3F3 px-11px text-16px" type="text"
:placeholder="$t('collectCode.payment.maxAmount', { currency: qrData.currency, price: qrData?.leftPrice })" @input="handleInput">
<input v-model="amount" class="w-272px h-48px bg-#F3F3F3 px-11px text-16px" type="text"
:placeholder="`${$t('collectCode.payment.maxAmount')} ${qrData.currency} ${qrData?.leftPrice}`" @input="handleInput">
</div>
<div class="text-#2B53AC text-14px" @click="changePayStatus">{{ payStatus === 1 ? $t('collectCode.payment.fullPayment') : $t('collectCode.payment.partialPayment') }}</div>
<div class="w-full mt-auto mb-40px">

View File

@ -1,11 +1,9 @@
<script setup>
import {VueSignaturePad} from 'vue-signature-pad';
import { showToast } from 'vant';
import { onMounted } from 'vue';
import {codeAuthStore} from "~/stores-collect-code/auth/index.js";
import {signOffline} from "~/api/goods/index.js";
import {useI18n} from "vue-i18n";
const {t} = useI18n();
const {formData,number}=codeAuthStore()
const signaturePad = ref(null);
@ -14,11 +12,10 @@ definePageMeta({
});
const router = useRouter();
const imgUrl = ref('');
const show = ref(false);
const goBack = () => {
router.back()
};
const submitSignature = () => {
if (signaturePad.value?.isEmpty()) {
showToast(t('collectCode.signature.pleaseSign'));
@ -26,7 +23,7 @@ const submitSignature = () => {
}
const { data } = signaturePad.value?.saveSignature();
imgUrl.value = data;
show.value = true;
confirm()
};
const clearSignature = () => {
@ -37,7 +34,7 @@ const confirm=async ()=>{
const res=await signOffline({
userInfo:formData.value,
signOrder:Number(number.value),
signImgFileData:imgUrl.value
signImgFileData:imgUrl.value,
})
if (res.status===0){
router.push('/collectCode/signature/result')
@ -51,6 +48,7 @@ const confirm=async ()=>{
<client-only>
<VueSignaturePad
width="100%"
height="93%"
class="signature bg-#fff rounded-10px mb-10px"
ref="signaturePad"
/>
@ -66,9 +64,6 @@ const confirm=async ()=>{
{{ $t('collectCode.signature.confirm') }}
</van-button>
</div>
<van-dialog v-model:show="show" show-cancel-button @confirm="confirm">
<img class="w-300px h-200px" :src="imgUrl" />
</van-dialog>
</div>
</div>
</template>

View File

@ -20,7 +20,7 @@ const columns = ref([
{text: t('realAuth.female'), value: 2},
])
const columns1 = ref([
{text: t('realAuth.idCard'), value: 1},
{text: t('realAuth.idTypeString'), value: 1},
{text: t('realAuth.passport'), value: 2},
{text: t('realAuth.other'), value: 3},
])
@ -72,7 +72,6 @@ const initData= async ()=>{
}
}
const nextClick=async ()=>{
console.log('number.value',number.value)
//
if (number.value==1){
if (!isFormComplete(formData.value)){
@ -91,6 +90,8 @@ const nextClick=async ()=>{
const res1=await signOffline({
userInfo:formData.value,
signOrder:Number(number.value),
testReturnHost:window.location.origin,
testReturnEndPoint:'/collectCode/signature/protocol',
})
if (res1.status===0){
window.location.href=res1.data.fddVerifyUrl
@ -137,8 +138,9 @@ initData()
<van-field :label="$t('realAuth.adress')" v-model="formData.address" class="mb-10px" :placeholder="$t('realAuth.adressPlaceholder')"/>
<van-field :label="$t('realAuth.bank')" v-model="formData.bankName" class="mb-10px" :placeholder="$t('realAuth.bankPlaceholder')"/>
<van-field :label="$t('realAuth.bankCard')" v-model="formData.bankNo" class="mb-10px" :placeholder="$t('realAuth.bankCardPlaceholder')"/>
<x-van-select v-model="formData.cardType" :label="$t('realAuth.idCard')" :columns="columns1"/>
<x-van-select v-model="formData.cardType" :label="$t('realAuth.idTye')" :columns="columns1"/>
<van-field :label="$t('realAuth.idCard')" v-model="formData.cardId" class="mb-10px" :placeholder="$t('realAuth.idCardPlaceholder')"/>
<x-van-select v-model="formData.gender" :label="$t('realAuth.gender')" :columns="columns"/>
</template>
</div>

View File

@ -39,8 +39,7 @@ const fetchPmblPdf = async () => {
})
pmblUrl.value = res.data?.viewUrl // PDF URLdata
} catch (error) {
console.error('获取拍卖笔录失败:', error)
}
}
}
//

View File

@ -8,7 +8,6 @@ definePageMeta({
i18n: 'countryRegion.title',
})
const router = useRouter()
console.log('router',router)
const { t, locale } = useI18n()
const value = ref('');
const alphabet = computed(() => {
@ -144,7 +143,6 @@ const handleCountrySelect = (country) => {
}
initData()
console.log('searchCountry',searchCountry.value)
//
watch(locale, () => {
initData()

View File

@ -1,15 +1,19 @@
<script setup>
import CheckoutPage from '@/components/stripe/CheckoutPage.vue'
import CompletePage from '@/components/stripe/CompletePage.vue'
definePageMeta({
layout: 'default',
i18n: 'menu.profile',
title: 'Stripe支付'
})
const route = useRoute()
const url=route.query.url??''
const key=route.query.key??''
</script>
<template>
<div>
<iframe :src="url"></iframe>
<CheckoutPage/>
<!-- <iframe class="w-100vw h-100vh" :src="`/stripe/checkout.html?key=${key}`"></iframe> -->
</div>
</template>

View File

@ -40,6 +40,26 @@ const openShow = async (item) => {
localState.value.showDetail = true
currentItem.value = item
}
/**
* 将数字格式化为"250XX"格式其中XX是两位数
* @param {number} num - 要格式化的数字
* @return {string} - 格式化后的字符串
*/
function formatNumber(num) {
//
if (typeof num !== 'number' && isNaN(Number(num))) {
throw new Error('输入必须是有效数字');
}
// ()
const number = Number(num);
// 0
const formattedNum = number.toString().padStart(2, '0');
// "250"
return `250${formattedNum}`;
}
</script>
<template>
@ -71,9 +91,9 @@ const openShow = async (item) => {
class="w-full object-cover rounded-4px"
/>
<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]"
class="absolute rounded-2px overflow-hidden line-height-12px left-[8px] top-[8px] h-[17px] w-[55px] flex items-center justify-center bg-[#2b53ac] text-[12px] text-[#fff]"
>
LOT{{ item.index }}
Lot{{ formatNumber(item.index) }}
</div>
</div>
<div class="pt-[8px]">

View File

@ -3,9 +3,9 @@ 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,getArtworkList} = goodStore();
const {auctionDetail,getArtworkList} = goodStore();
const {fullLive} = liveStore()
const changeLive = () => {
if (!fullLive.value){
@ -16,7 +16,7 @@ const changeLive = () => {
}
}
await getAuctionDetail()
</script>
<template>
<div class="grow-1">
@ -27,13 +27,13 @@ await getAuctionDetail()
<div class="text-18px mb-5px">{{ auctionDetail.title }}</div>
<div class="text-12px mb-54px">{{ $t('home.text1') }}<van-icon name="arrow" /></div>
<div><span>-</span> <span class="text-12px mx-5px">{{auctionDetail.totalNum}}{{ $t('common.items') }}{{ $t('common.auction') }}</span> <span>-</span></div>
<div class="text-12px">{{auctionDetail.startDate}} {{$t('home.text2')}}</div>
<div class="text-12px">{{auctionDetail.startDate}} {{auctionDetail.startTitle}}</div>
</div>
<div v-else class="absolute h-188px w-screen pt-36px flex flex-col text-#fff top-0 left-0 items-center bg-[url('@/static/images/z6022@2x.png')]">
<div class="text-18px mb-5px">{{ auctionDetail.title }}</div>
<div class="text-12px mb-54px">本场拍卖会{{auctionDetail.isLiving===2?'未开始':'已结束'}}</div>
<div class="text-12px mb-54px">{{$t('home.text3')}}{{auctionDetail.isLiving===2?$t('home.text4'):$t('home.text5')}}</div>
<div><span>-</span> <span class="text-12px mx-5px">{{auctionDetail.totalNum}}{{ $t('common.items') }}{{ $t('common.auction') }}</span> <span>-</span></div>
<div class="text-12px">{{auctionDetail.startDate}} {{$t('home.text2')}}</div>
<div class="text-12px">{{auctionDetail.startDate}} {{auctionDetail.startTitle}}</div>
</div>
</div>
</client-only>

View File

@ -40,7 +40,7 @@ const headItem=(statusCode)=>{
<div class="text-#939393 text-14px">{{ $t('live_room.next_lot') }}</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 v-for="(item, index) in auctionData.auctionPriceList?.buys" :key="index" class="flex flex-shrink-0">
<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'? $t('live_room.spot'):$t('live_room.network') }}</div>
<div class="text-start shrink-0 w-80px">{{ item.createdAt }}</div>

View File

@ -30,7 +30,6 @@ const captureVideoFrame = () => {
try {
const video = document.querySelector('#J_prismPlayer video')
if (!video) {
console.error('未找到视频元素')
return null
}
@ -42,7 +41,6 @@ const captureVideoFrame = () => {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
return canvas.toDataURL('image/jpeg', 0.9)
} catch (error) {
console.error('获取视频截图失败:', error)
return null
}
}

View File

@ -118,7 +118,6 @@ const captureVideoFrame = () => {
try {
const video = document.querySelector('#J_prismPlayer video')
if (!video) {
console.error('未找到视频元素')
return null
}
@ -130,7 +129,6 @@ const captureVideoFrame = () => {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
return canvas.toDataURL('image/jpeg', 0.9)
} catch (error) {
console.error('获取视频截图失败:', error)
return null
}
}
@ -144,8 +142,7 @@ const handleCapture = () => {
onClick:()=>{
router.replace('/')
fullLive.value=true
console.log('执行')
}
}
})*!/
}*/
showMinWindow1({

View File

@ -66,8 +66,7 @@ const loadMore = async () => {
watch(()=>{
return auctionData.value?.artwork?.index
},(newValue)=>{
console.log('newValue',newValue)
})
})
watch(()=>props.show,(newValue)=>{
if (newValue){
nextTick(()=>{

View File

@ -13,16 +13,12 @@ import {message} from "~/components/x-message/useMessage.js"
import {showConfirmDialog} from 'vant';
import {artworkBuy} from "@/api/goods/index.js"
import {useI18n} from 'vue-i18n'
import {CountUp} from 'countup.js'
const { t } = useI18n()
const countUpRef = ref(null)
const nextPriceRef = ref(null)
const { auctionDetail} = goodStore();
const { auctionDetail,getAuctionDetail} = goodStore();
const player = ref(null)
const {quoteStatus, show, playerId, show1, auctionData, getSocketData, getLiveLink, fullLive} = liveStore()
const pullLink = ref('')
const handlePlayerError = (error) => {
console.error('播放器错误:', error)
showConfirmDialog({
message: t('live_room.error_mess'),
showCancelButton: true
@ -84,8 +80,7 @@ const initializePlayer = async () => {
})
player.value.on('loading', () => {
console.log('loading')
})
})
player.value.on('error', handlePlayerError)
} catch (error) {
showConfirmDialog({
@ -95,12 +90,12 @@ const initializePlayer = async () => {
initializePlayer()
}).catch(() => {
})
console.error('播放器初始化失败:', error)
}
}
}
onMounted(async () => {
await getAuctionDetail()
pullLink.value = await getLiveLink()
if (auctionDetail.value.isLiving===1){
initializePlayer()

View File

@ -19,6 +19,7 @@ const loadingRef=ref({
})
const isExist=ref(false)// true
const isReal=ref(false) //isReal
const codeInput=ref(null)
function goToPage() {
router.push('/countryRegion');
}
@ -40,11 +41,9 @@ const countdown = ref(0);
const phoneNum = ref('')
const code = ref('')
const pane = ref(0)
const showKeyboard = ref(false);
//
const getDefaultCountry = () => {
let defaultCode = 'CN' //
console.log('locale.value',locale.value)
switch (locale.value) {
case 'zh-CN':
defaultCode = 'CN'
@ -74,7 +73,6 @@ const defaultCountry = getDefaultCountry()
const selectedCountry = ref(route.query.countryName || defaultCountry.name)
console.log('selectedCountry',selectedCountry.value)
onMounted(()=>{
selectedZone.value=route.query.zone || defaultCountry.zone
})
@ -100,13 +98,8 @@ const getCode =async () => {
}
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 = ''
@ -136,8 +129,7 @@ const goLogin =async () => {
if (res1.status===0){
window.location.href=res1.data.h5Url
}
console.log('123')
}else {
}else {
await router.push('/');
}
}
@ -145,7 +137,7 @@ const goLogin =async () => {
}
const isKeyboardVisible = ref(false)
const windowHeight = ref(window.innerHeight)
const isFocused = ref(false)
onMounted(() => {
//
windowHeight.value = window.innerHeight
@ -163,7 +155,7 @@ onUnmounted(() => {
</script>
<template>
<div class="h-screen-nav w-[100vw] bg-[url('@/static/images/asdfsdd.png')] bg-cover px-[31px] pt-[86px]">
<div class="w-[100vw] bg-[url('@/static/images/asdfsdd.png')] bg-bottom bg-cover grow-1 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>
@ -186,7 +178,6 @@ onUnmounted(() => {
</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>
@ -200,7 +191,24 @@ onUnmounted(() => {
<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="relative">
<van-password-input
:value="code"
:gutter="10"
:mask="false"
:focused="isFocused"
/>
<input
v-model="code"
type="tel"
maxlength="6"
ref="codeInput"
class="opacity-0 absolute top-0 left-0 h-full w-full"
@input="code = $event.target.value.replace(/\D/g, '').slice(0, 6)"
@focus="isFocused = true"
@blur="isFocused = false"
/>
</div>
<div class="flex justify-between">
<div :class="`${countdown>0?'text-#BDBDBD':'text-#2B53AC'} text-14px`">
{{ $t('login.reSend') }}<span v-if="countdown>0">({{countdown}})</span>
@ -216,7 +224,6 @@ onUnmounted(() => {
</div>
</van-swipe-item>
</van-swipe>
<van-number-keyboard v-model="code" :show="showKeyboard" @blur="showKeyboard = false" />
<div v-if="!isKeyboardVisible" class="text-center text-14px absolute left-1/2 transform translate-x--1/2 bottom-20px">
{{ $t('login.agreement') }}<span class="text-#3454AF " @click="$router.push('/privacyPolicy')">{{ $t('login.privacyPolicy') }}</span>
</div>

View File

@ -0,0 +1,363 @@
<script setup>
import { onMounted, ref } from 'vue'
const config = useRuntimeConfig()
const stripe = Stripe(config.public.NUXT_PUBLIC_PKEY)
const statusIcon = ref('')
const statusText = ref('')
const intentId = ref('')
const intentStatus = ref('')
const viewDetailsUrl = ref('')
const iconColor = ref('')
const SuccessIcon = `<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4695 0.232963C15.8241 0.561287 15.8454 1.1149 15.5171 1.46949L6.14206 11.5945C5.97228 11.7778 5.73221 11.8799 5.48237 11.8748C5.23253 11.8698 4.99677 11.7582 4.83452 11.5681L0.459523 6.44311C0.145767 6.07557 0.18937 5.52327 0.556912 5.20951C0.924454 4.89575 1.47676 4.93936 1.79051 5.3069L5.52658 9.68343L14.233 0.280522C14.5613 -0.0740672 15.1149 -0.0953599 15.4695 0.232963Z" fill="white"/>
</svg>`
const ErrorIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.25628 1.25628C1.59799 0.914573 2.15201 0.914573 2.49372 1.25628L8 6.76256L13.5063 1.25628C13.848 0.914573 14.402 0.914573 14.7437 1.25628C15.0854 1.59799 15.0854 2.15201 14.7437 2.49372L9.23744 8L14.7437 13.5063C15.0854 13.848 15.0854 14.402 14.7437 14.7437C14.402 15.0854 13.848 15.0854 13.5063 14.7437L8 9.23744L2.49372 14.7437C2.15201 15.0854 1.59799 15.0854 1.25628 14.7437C0.914573 14.402 0.914573 13.848 1.25628 13.5063L6.76256 8L1.25628 2.49372C0.914573 2.15201 0.914573 1.59799 1.25628 1.25628Z" fill="white"/>
</svg>`
const InfoIcon = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 1.5H4C2.61929 1.5 1.5 2.61929 1.5 4V10C1.5 11.3807 2.61929 12.5 4 12.5H10C11.3807 12.5 12.5 11.3807 12.5 10V4C12.5 2.61929 11.3807 1.5 10 1.5ZM4 0C1.79086 0 0 1.79086 0 4V10C0 12.2091 1.79086 14 4 14H10C12.2091 14 14 12.2091 14 10V4C14 1.79086 12.2091 0 10 0H4Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 7C5.25 6.58579 5.58579 6.25 6 6.25H7.25C7.66421 6.25 8 6.58579 8 7V10.5C8 10.9142 7.66421 11.25 7.25 11.25C6.83579 11.25 6.5 10.9142 6.5 10.5V7.75H6C5.58579 7.75 5.25 7.41421 5.25 7Z" fill="white"/>
<path d="M5.75 4C5.75 3.31075 6.31075 2.75 7 2.75C7.68925 2.75 8.25 3.31075 8.25 4C8.25 4.68925 7.68925 5.25 7 5.25C6.31075 5.25 5.75 4.68925 5.75 4Z" fill="white"/>
</svg>`
function setPaymentDetails(intent) {
let status = "Something went wrong, please try again."
let color = "#DF1B41"
let icon = ErrorIcon
if (!intent) {
setErrorState()
return
}
switch (intent.status) {
case "succeeded":
status = "Payment succeeded"
color = "#30B130"
icon = SuccessIcon
break
case "processing":
status = "Your payment is processing."
color = "#6D6E78"
icon = InfoIcon
break
case "requires_payment_method":
status = "Your payment was not successful, please try again."
break
}
iconColor.value = color
statusIcon.value = icon
statusText.value = status
intentId.value = intent.id
intentStatus.value = intent.status
viewDetailsUrl.value = `https://dashboard.stripe.com/payments/${intent.id}`
}
function setErrorState() {
iconColor.value = "#DF1B41"
statusIcon.value = ErrorIcon
statusText.value = "Something went wrong, please try again."
}
async function checkStatus() {
const urlParams = new URLSearchParams(window.location.search)
const clientSecret = urlParams.get("payment_intent_client_secret")
if (!clientSecret) {
setErrorState()
return
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret)
setPaymentDetails(paymentIntent)
router.push({
path:'/payment/result',
query:{
orderNo:route.query.orderNo
}
})
}
const router=useRouter()
const route=useRoute()
onMounted(() => {
checkStatus()
})
</script>
<template>
<div id="payment-status">
<div id="status-icon" :style="{ backgroundColor: iconColor }" v-html="statusIcon"></div>
<h2 id="status-text">{{ statusText }}</h2>
<div id="details-table">
<table>
<tbody>
<tr>
<td class="TableLabel">id</td>
<td id="intent-id" class="TableContent">{{ intentId }}</td>
</tr>
<tr>
<td class="TableLabel">status</td>
<td id="intent-status" class="TableContent">{{ intentStatus }}</td>
</tr>
</tbody>
</table>
</div>
<a :href="viewDetailsUrl" id="view-details" rel="noopener noreferrer" target="_blank">
View details
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 3.49998C2.64175 3.49998 2.25 3.89173 2.25 4.37498V11.375C2.25 11.8582 2.64175 12.25 3.125 12.25H10.125C10.6082 12.25 11 11.8582 11 11.375V9.62498C11 9.14173 11.3918 8.74998 11.875 8.74998C12.3582 8.74998 12.75 9.14173 12.75 9.62498V11.375C12.75 12.8247 11.5747 14 10.125 14H3.125C1.67525 14 0.5 12.8247 0.5 11.375V4.37498C0.5 2.92524 1.67525 1.74998 3.125 1.74998H4.875C5.35825 1.74998 5.75 2.14173 5.75 2.62498C5.75 3.10823 5.35825 3.49998 4.875 3.49998H3.125Z" fill="#0055DE"/>
<path d="M8.66672 0C8.18347 0 7.79172 0.391751 7.79172 0.875C7.79172 1.35825 8.18347 1.75 8.66672 1.75H11.5126L4.83967 8.42295C4.49796 8.76466 4.49796 9.31868 4.83967 9.66039C5.18138 10.0021 5.7354 10.0021 6.07711 9.66039L12.7501 2.98744V5.83333C12.7501 6.31658 13.1418 6.70833 13.6251 6.70833C14.1083 6.70833 14.5001 6.31658 14.5001 5.83333V0.875C14.5001 0.391751 14.1083 0 13.6251 0H8.66672Z" fill="#0055DE"/>
</svg>
</a>
<NuxtLink id="retry-button" to="/checkout">Test another</NuxtLink>
</div>
</template>
<style scoped>
/* Variables */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
height: 100vh;
width: 100vw;
}
form {
width: 30vw;
min-width: 500px;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
margin-top: auto;
margin-bottom: auto;
}
.hidden {
display: none;
}
#payment-message {
color: rgb(105, 115, 134);
font-size: 16px;
line-height: 20px;
padding-top: 12px;
text-align: center;
}
#payment-element {
margin-bottom: 24px;
}
/* Buttons and links */
button {
background: #0055DE;
font-family: Arial, sans-serif;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: block;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
button:hover {
filter: contrast(115%);
}
button:disabled {
opacity: 0.5;
cursor: default;
}
/* spinner/processing state, errors */
.spinner,
.spinner:before,
.spinner:after {
border-radius: 50%;
}
.spinner {
color: #ffffff;
font-size: 22px;
text-indent: -99999px;
margin: 0px auto;
position: relative;
width: 20px;
height: 20px;
box-shadow: inset 0 0 0 2px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before,
.spinner:after {
position: absolute;
content: "";
}
.spinner:before {
width: 10.4px;
height: 20.4px;
background: #0055DE;
border-radius: 20.4px 0 0 20.4px;
top: -0.2px;
left: -0.2px;
-webkit-transform-origin: 10.4px 10.2px;
transform-origin: 10.4px 10.2px;
-webkit-animation: loading 2s infinite ease 1.5s;
animation: loading 2s infinite ease 1.5s;
}
.spinner:after {
width: 10.4px;
height: 10.2px;
background: #0055DE;
border-radius: 0 10.2px 10.2px 0;
top: -0.1px;
left: 10.2px;
-webkit-transform-origin: 0px 10.2px;
transform-origin: 0px 10.2px;
-webkit-animation: loading 2s infinite ease;
animation: loading 2s infinite ease;
}
/* Payment status page */
#payment-status {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
row-gap: 30px;
width: 30vw;
min-width: 500px;
min-height: 380px;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
opacity: 0;
animation: fadeInAnimation 1s ease forwards;
}
#status-icon {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
width: 40px;
border-radius: 50%;
}
h2 {
margin: 0;
color: #30313D;
text-align: center;
}
a {
text-decoration: none;
font-size: 16px;
font-weight: 600;
font-family: Arial, sans-serif;
display: block;
}
a:hover {
filter: contrast(120%);
}
#details-table {
overflow-x: auto;
width: 100%;
}
table {
width: 100%;
font-size: 14px;
border-collapse: collapse;
}
table tbody tr:first-child td {
border-top: 1px solid #E6E6E6; /* Top border */
padding-top: 10px;
}
table tbody tr:last-child td {
border-bottom: 1px solid #E6E6E6; /* Bottom border */
}
td {
padding-bottom: 10px;
}
.TableContent {
text-align: right;
color: #6D6E78;
}
.TableLabel {
font-weight: 600;
color: #30313D;
}
#view-details {
color: #0055DE;
}
#retry-button {
text-align: center;
background: #0055DE;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
@-webkit-keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes fadeInAnimation {
to {
opacity: 1;
}
}
@media only screen and (max-width: 600px) {
form, #payment-status{
width: 80vw;
min-width: initial;
}
}
</style>

View File

@ -4,8 +4,9 @@ import {createBuyOrder} from "~/api/goods/index.js";
import {goodStore} from "~/stores/goods/index.js";
import { showLoadingToast ,closeToast} from 'vant';
import {authStore} from "~/stores/auth/index.js";
import {message} from "~/components/x-message/useMessage.js";
const {checkoutSessionUrl,payment}= authStore()
const {checkoutSessionUrl,payment,payUid}= authStore()
const payStatus=ref(0)
definePageMeta({
i18n: 'payment.title'
@ -30,6 +31,7 @@ const confirmPay=async ()=>{
message: t('payment.loading'),
forbidClick: true,
});
const res=await createBuyOrder({
buyUid:payment.value.buyUid,
price:payStatus.value===0?payment.value.leftPrice:amount.value,
@ -37,17 +39,20 @@ const confirmPay=async ()=>{
testReturnHost:window.location.origin,
testReturnEndPoint:'/payment/result'
})
if (res.status===0){
/* router.push({
path:'/externallinks',
query:{
url:res.data.checkoutSessionUrl
}
})*/
window.location.href=res.data.checkoutSessionUrl
}
}
if (res.status===0){
checkoutSessionUrl.value=res.data.checkoutSessionUrl
payUid.value=res.data.payUid
router.push({
path:'/checkoutPage',
query:{
payUid:res.data.payUid,
returnUrl:'/payment/result',
stripeKey:res.data.checkoutSessionUrl
}
})
}
}
const handleInput = (e) => {
//
const value = e.target.value

View File

@ -1,44 +1,94 @@
<script setup>
import {orderQuery} from "~/api/goods/index.js";
import { showLoadingToast, closeToast } from 'vant';
definePageMeta({
i18n: 'payment.text1',
})
const router = useRouter()
const {t}=useI18n();
const {t} = useI18n();
const route = useRoute();
const resData=ref({})
const res=await orderQuery({
orderNo:route.query.orderNo
const resData = ref({})
let timer = null
let startTime = Date.now()
const queryOrder = async () => {
// 5
if (Date.now() - startTime > 5000) {
clearInterval(timer)
closeToast()
return
}
showLoadingToast({
message: '加载中...',
forbidClick: true,
});
try {
const res = await orderQuery({
orderNo: route.query.orderNo
})
if (res.status === 0) {
resData.value = res.data
//
if (resData.value.status === 1) {
clearInterval(timer)
closeToast()
}
}
} catch (error) {
clearInterval(timer)
closeToast()
}
}
//
await queryOrder()
//
timer = setInterval(async () => {
await queryOrder()
}, 1000)
//
onUnmounted(() => {
if (timer) {
clearInterval(timer)
closeToast()
}
})
if (res.status===0){
resData.value=res.data
const statusLabel = {
1: t('payment.text2'),
2: t('payment.text3'),
3: t('payment.text4'),
4: t('payment.text5'),
}
const statusLabel={
1:t('payment.text2'),
2:t('payment.text3'),
3:t('payment.text4'),
4:t('payment.text5'),
}
const goHome=()=>{
const goHome = () => {
router.push('/')
}
</script>
<template>
<div class="w-[100vw] h-screen-nav bg-[url('@/static/images/3532@2x.png')] bg-cover grow-1 flex flex-col items-center px-30px">
<div
class="w-[100vw] h-screen-nav bg-[url('@/static/images/3532@2x.png')] bg-cover grow-1 flex flex-col items-center px-30px">
<div class="flex flex-col items-center mt-150px">
<img class="w-119px h-120px mb-36px" src="@/static/images/5554@2x1.png" alt="">
<div class="text-#000 text-16px mb-25px">{{statusLabel[resData.status]}}!</div>
<div class="text-#999 text-16px">{{resData.currency}}{{resData.money}}</div>
<div class="text-#000 text-16px mb-25px">{{ statusLabel[resData.status] }}!</div>
<div class="text-#999 text-16px">{{ resData.currency }}{{ resData.money }}</div>
</div>
<div class="w-full mt-auto mb-40px">
<van-button type="primary" block @click="goHome">
{{ t('payment.backToHome') }}
{{ t('payment.result.backToHome') }}
</van-button>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -37,8 +37,7 @@ const fetchData = async () => {
showMyList.value = groupByDate(res.data.data)
}
} catch (error) {
console.error('获取数据失败:', error)
}
}
}
const onRefresh = async () => {
@ -78,6 +77,13 @@ fetchData()
</div>
</div>
<!-- 设置选项 -->
<div class="px-16px mb-20px">
<van-cell-group inset>
<!-- 移除语言设置入口 -->
</van-cell-group>
</div>
<!-- 列表内容 -->
<div class="grow-1 flex flex-col">
<div class="border-b-1px border-b-#D3D3D3 px-16px">

View File

@ -1,13 +1,18 @@
<script setup>
import {authStore} from "@/stores/auth/index.js";
import {useI18n} from 'vue-i18n'
const {t:$t} = useI18n()
const {t} = useI18n()
const props = defineProps({
type: {
type: Number,
default: 0
}
})
const columns1 = ref([
{text: t('realAuth.idCard'), value: '1'},
{text: t('realAuth.passport'), value: '2'},
{text: t('realAuth.other'), value: '3'},
])
const {userInfo}= authStore()
</script>
@ -35,7 +40,7 @@ const {userInfo}= authStore()
<template v-if="type===1">
<div class="flex mb-20px" >
<div class="mr-10px">{{$t('realAuth.name')}}</div>
<div>{{userInfo.realName}}</div>
<div>{{userInfo.realName||userInfo.userExtend.realName||''}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">{{$t('realAuth.gender')}}</div>
@ -57,6 +62,14 @@ const {userInfo}= authStore()
<div class="mr-10px">{{$t('realAuth.bankCard')}}</div>
<div>{{userInfo.userExtend.bankNo}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">{{$t('realAuth.idTye')}}</div>
<div>{{columns1.find(x=>x.value===userInfo.userExtend.idType)?.text}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">{{$t('realAuth.idNumber')}}</div>
<div>{{userInfo.userExtend.idNo}}</div>
</div>
</template>
</div>
</template>

View File

@ -17,6 +17,11 @@ const { locale } = useI18n()
const {userInfo,selectedZone}= authStore()
const active=ref(locale.value==='zh-CN'?0:1)
const { t } = useI18n()
const columns1 = ref([
{text: t('realAuth.idCard'), value: '1'},
{text: t('realAuth.passport'), value: '2'},
{text: t('realAuth.other'), value: '3'},
])
const form=ref({
realName: "",
sex:'',
@ -124,6 +129,12 @@ const goLogin=()=>{
<van-field v-model="form.userExtend.bankNo" :label="$t('realAuth.bankCard')" clearable
:placeholder="$t('realAuth.bankCardPlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<x-van-select v-model="form.userExtend.idType" :label="$t('realAuth.idTye')" :columns="columns1"/>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field :label="$t('realAuth.idNumber')" v-model="form.userExtend.idNo" class="mb-10px" :placeholder="$t('realAuth.idNumberPlaceholder')"/>
</div>
</div>
</van-tab>
</van-tabs>
@ -135,7 +146,7 @@ const goLogin=()=>{
<detail :type="active"></detail>
</van-tab>
</van-tabs>
<div class="flex justify-between" v-if="statusCode===0">
<div class="flex justify-between shrink-0 mb-20px" v-if="statusCode===0">
<van-button style="width: 151px;height: 48px" color="#E9F1F8" @click="goLogin">
<div class="text-#2B53AC text-16px">{{ $t('realAuth.cancel') }}</div>
</van-button>

View File

@ -1,5 +1,5 @@
<script setup>
import {showToast} from 'vant';
import {showToast,showLoadingToast } from 'vant';
import {onMounted, onUnmounted, ref} from 'vue';
import {signOffline, signOnline} from "~/api/goods/index.js";
import {VueSignaturePad} from "vue-signature-pad";
@ -10,55 +10,26 @@ const {t:$t} = useI18n()
definePageMeta({
layout: ''
})
const { userInfo ,payment} = authStore()
const { payment} = authStore()
const signaturePad = ref(null);
const isLandscapeMode = ref(false);
const checkScreenOrientation = () => {
const orientation = screen.orientation?.type || window.orientation;
if (orientation === 'landscape-primary' || orientation === 'landscape-secondary' ||
orientation === 90 || orientation === -90) {
isLandscapeMode.value = true;
} else {
isLandscapeMode.value = false;
showToast($t('signature.tips.landscape'));
}
};
onMounted(() => {
nextTick(() => {
checkScreenOrientation();
});
window.addEventListener('orientationchange', checkScreenOrientation);
screen.orientation?.addEventListener('change', checkScreenOrientation);
});
onUnmounted(() => {
window.removeEventListener('orientationchange', checkScreenOrientation);
screen.orientation?.removeEventListener('change', checkScreenOrientation);
});
const imgUrl = ref('')
const show = ref(false)
const clearSignature = () => {
signaturePad.value?.clearSignature();
};
const toast=ref(false)
const submitSignature = () => {
if (signaturePad.value?.isEmpty()) {
showToast($t('signature.pleaseSign'));
showToast($t('collectCode.signature.pleaseSign'));
return;
}
toast.value=showLoadingToast({
message: '加载中...',
forbidClick: true,
});
const { data } = signaturePad.value?.saveSignature(); // base64
imgUrl.value = data;
show.value = true;
nextTick(() => {
const overlay = document.querySelector('.signature-container .van-overlay');
if (overlay) {
overlay.style.width = '100vw';
overlay.style.left = '0';
overlay.style.right = '0';
}
})
confirm()
};
const confirm = async () => {
const res = await signOnline({
@ -66,7 +37,8 @@ const confirm = async () => {
signImgFileData: imgUrl.value
})
if (res.status===0){
router.push('/payment')
await router.push('/payment')
toast.value?.close()
}
}
const goBack = () => {
@ -75,50 +47,28 @@ router.back()
</script>
<template>
<div class="signature-container bg-gray ">
<template v-if="isLandscapeMode">
<div class="signature-content px-10px py-10px">
<div class="signature-container">
<div class="flex flex-col h-100vh px-20px py-20px bg-gray w-100vw">
<client-only>
<VueSignaturePad
width="100%"
height="93%"
class="signature bg-#fff rounded-10px mb-10px"
ref="signaturePad"
/>
<div class="control-buttons justify-evenly">
<van-button
class="control-button"
size="mini"
type="primary"
@click="goBack"
>
{{ $t('signature.action.back') }}
</van-button>
<van-button
class="control-button"
size="mini"
type="warning"
@click="clearSignature"
>
{{ $t('signature.action.clear') }}
</van-button>
<van-button
class="control-button"
size="mini"
type="primary"
@click="submitSignature"
>
{{ $t('signature.action.confirm') }}
</van-button>
</div>
</client-only>
<div class="flex justify-evenly">
<van-button class="!h-40px mr-15px" type="primary" @click="goBack">
{{ $t('collectCode.signature.back') }}
</van-button>
<van-button class="!h-40px" type="warning" @click="clearSignature">
{{ $t('collectCode.signature.clear') }}
</van-button>
<van-button class="!h-40px" type="primary" @click="submitSignature">
{{ $t('collectCode.signature.confirm') }}
</van-button>
</div>
</template>
<template v-else>
<div class="orientation-hint">
<p>{{$t('signature.tips.landscape')}}</p>
</div>
</template>
<van-dialog v-model:show="show" class="signature-dialog" show-cancel-button @confirm="confirm">
<img class="h-100px" :src="imgUrl"/>
</van-dialog>
</div>
</div>
</template>

View File

@ -33,8 +33,7 @@ const fetchPmblPdf = async () => {
})
pmblUrl.value = res.data?.viewUrl // PDF URLdata
} catch (error) {
console.error('获取拍卖笔录失败:', error)
}
}
}
//

View File

@ -1,5 +1,6 @@
import { setupHttp } from '@/api/http'
import { setupHttp as setupHttp1} from '@/api-collect-code/http'
export default defineNuxtPlugin(() => {
setupHttp()
setupHttp1()
})

View File

@ -15,22 +15,28 @@ export default defineNuxtPlugin(() => {
if (import.meta.client) {
const i18n = useNuxtApp().$i18n
const { setLocale } = i18n
const nuxtApp = useNuxtApp()
// 暂时设置固定语言,用于调试
// 可以根据需要修改这里的语言代码:'zh-CN' | 'en-US' | 'ja-JP' | 'zh-TW'
const fixedLang = 'zh-CN'
setLocale(fixedLang)
Locale.use(fixedLang)
// 原自动检测系统语言的逻辑(暂时注释)
/* const lang = localStorage.getItem('lang')
if (lang) {
setLocale(lang as TypeLocale)
Locale.use(lang)
// 获取系统语言
const getSystemLanguage = () => {
const browserLang = navigator.language
// 将浏览器语言映射到应用支持的语言
if (browserLang.startsWith('zh')) {
return browserLang.includes('TW') || browserLang.includes('HK') ? 'zh-TW' : 'zh-CN'
} else if (browserLang.startsWith('ja')) {
return 'ja-JP'
} else if (browserLang.startsWith('en')) {
return 'en-US'
}
// 默认返回中文
return 'zh-CN'
}
else {
setLocale(i18n.locale.value)
Locale.use(i18n.locale.value)
}*/
// 使用系统语言
const systemLang = getSystemLanguage()
setLocale(systemLang as TypeLocale)
Locale.use(systemLang)
}
})

View File

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

View File

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

Binary file not shown.

View File

@ -1,6 +1,6 @@
import { createGlobalState,useLocalStorage } from '@vueuse/core'
export const codeAuthStore = createGlobalState(() => {
const token=useLocalStorage('token','')
const codeToken=useLocalStorage('codeToken','')
const RefreshToken=useLocalStorage('RefreshToken','')
const userInfo=useLocalStorage('userInfo',{})
const fingerprint=useLocalStorage('fingerprint','')
@ -26,7 +26,11 @@ export const codeAuthStore = createGlobalState(() => {
currency:''
})
const qrData=useLocalStorage('qrData',{})
const codePKey=useLocalStorage('codePKey','')
const codePayUid=useLocalStorage('codePayUid','')
return{
codePKey,
codePayUid,
qrData,
qrUid,
cpayment,
@ -37,7 +41,7 @@ export const codeAuthStore = createGlobalState(() => {
formData,
userInfo,
RefreshToken,
token,
codeToken,
fingerprint
}
})

View File

@ -1,4 +1,7 @@
import { createGlobalState,useLocalStorage } from '@vueuse/core'
import { WebSocketClient } from '@/utils/websocket'
import {message} from "~/components/x-message/useMessage.js";
export const authStore = createGlobalState(() => {
const token=useLocalStorage('token','')
const RefreshToken=useLocalStorage('RefreshToken','')
@ -12,8 +15,9 @@ export const authStore = createGlobalState(() => {
buyUid:'',
auctionArtworkUuid:''
})
const payUid=useLocalStorage('payUid','')
return{
payUid,
selectedZone,
payment,
checkoutSessionUrl,

View File

@ -34,8 +34,7 @@ export const goodStore = createGlobalState(() => {
auctionDetail.value = res.data
}
} catch (err) {
console.error('获取拍卖详情错误:', err)
} finally {
} finally {
loading.value = false
}
}
@ -69,7 +68,6 @@ export const goodStore = createGlobalState(() => {
}
return { finished: true, items: [] }
} catch (err) {
console.error('获取艺术品列表错误:', err)
return { finished: true, items: [] }
} finally {
loading.value = false
@ -85,8 +83,7 @@ export const goodStore = createGlobalState(() => {
artWorkDetail.value = res.data
}
} catch (err) {
console.error('获取艺术品详情错误:', err)
} finally {
} finally {
loading.value = false
}
}

View File

@ -197,12 +197,10 @@ export const liveStore = createGlobalState(() => {
// WebSocket 事件处理
ws.onOpen(() => {
console.log('WebSocket connected')
})
})
ws.onMessage((data) => {
auctionData.value = data.data
console.log(' auctionData.value', auctionData.value)
const { wsType, tip } = data.data || {}
switch (wsType) {
@ -210,13 +208,11 @@ export const liveStore = createGlobalState(() => {
handleTipMessage(tip?.tipType)
break
case WS_TYPES.STOP_ARTWORK:
console.log('changeQuote',quoteStatus.value)
//quoteStatus.value = false
break
case WS_TYPES.OVER:
quoteStatus.value = false
console.log('changeQuote',quoteStatus.value)
message.success(createMessageConfig(
t('live_room.text10'),
'#575757',
@ -230,16 +226,13 @@ export const liveStore = createGlobalState(() => {
break
}
console.log('onmessage', data)
})
})
ws.onClose(() => {
console.log('WebSocket disconnected')
})
})
ws.onError((error) => {
console.error('WebSocket error:', error)
})
})
}
const changeStatus = () => {
if (auctionData.value.artwork?.isSelling&&!auctionData.value.artwork.isSoled){
@ -249,8 +242,7 @@ export const liveStore = createGlobalState(() => {
quoteStatus.value = false
}
}
console.log('changeQuote',quoteStatus.value)
}
}
return{
fullLive,
isMinWindow,

View File

@ -1,44 +0,0 @@
/**
* 格式化价格
* @param {number} price - 价格数值
* @param {string} currency - 货币符号默认为 ¥
* @returns {string} 格式化后的价格字符串
*/
export const formatPrice = (price, currency = '¥') => {
if (price == null || isNaN(price)) return `${currency}0`
// 将价格转换为数字
const numPrice = Number(price)
// 处理小数点,保留两位小数
const formattedPrice = numPrice.toFixed(2)
// 添加千位分隔符
const parts = formattedPrice.toString().split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return `${currency}${parts.join('.')}`
}
/**
* 格式化数字
* @param {number} num - 需要格式化的数字
* @returns {string} 格式化后的数字字符串
*/
export const formatNumber = (num) => {
if (num == null || isNaN(num)) return '0'
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
/**
* 格式化百分比
* @param {number} value - 需要格式化的值
* @param {number} decimals - 小数位数默认为 2
* @returns {string} 格式化后的百分比字符串
*/
export const formatPercent = (value, decimals = 2) => {
if (value == null || isNaN(value)) return '0%'
return `${(Number(value) * 100).toFixed(decimals)}%`
}

View File

@ -1,10 +0,0 @@
export default function preload() {
return `
;(function() {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const setting = localStorage.getItem('nuxt-color-mode') || 'auto';
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('van-theme-dark', true);
})()
`
}

5
env/.env.prod vendored
View File

@ -1,4 +1,5 @@
# 生产环境配置
NUXT_PUBLIC_API_BASE=https://auction.yixunlink.com
NUXT_PUBLIC_SOCKET_URL=wss://auction.yixunlink.com
NUXT_PUBLIC_API_BASE=https://auction.szjixun.cn
NUXT_PUBLIC_SOCKET_URL=wss://auction.szjixun.cn
NUXT_API_SECRET=prod-secret
NUXT_PUBLIC_PKEY=pk_live_51QfbSAAB1Vm8VfJqEVY2uFHPn9N4sDbOaCzht8IVKoylYBrYvdUsmsnCzGxIoN9skBCvI5PsxLJcf4PlytXIr1aX00mFJBXSB8

3
env/.env.test vendored
View File

@ -2,4 +2,5 @@
NUXT_PUBLIC_API_BASE=https://auction-test.szjixun.cn
NUXT_PUBLIC_API_COLLECT_CODE=http://auction-test.szjixun.cn
NUXT_API_SECRET=test-secret
NUXT_PUBLIC_SOCKET_URL=wss://auction-test.szjixun.cn
NUXT_PUBLIC_SOCKET_URL=wss://auction-test.szjixun.cn
NUXT_PUBLIC_PKEY=pk_test_51QfbSAAB1Vm8VfJq3AWsR4k2mZjnlF7XFrmlbc6XVXrtwXquAUfwzZmOFDbxMIAwqJBgqao8KLt2wmPc4vNOCTeo00WB78KtfV

View File

@ -133,8 +133,8 @@
"otherTab": "Non-Mainland Residents",
"cnTabDesc": "Please fill in ID card information",
"otherTabDesc": "Please upload personal information",
"idCard": "ID Card Number",
"idCardPlaceholder": "Please enter ID card number",
"idCard": "ID number",
"idCardPlaceholder": "Please enter your ID number",
"name": "Name",
"namePlaceholder": "Please enter your name",
"gender": "Gender",
@ -157,7 +157,13 @@
"male": "Male",
"female": "Female"
}
}
},
"idTye": "Document Type",
"passport": "passport",
"other": "other",
"idNumber": "ID number",
"idNumberPlaceholder": "Please enter your ID number",
"idTypeString": "ID card"
},
"detail": {
"text1": "Artist",
@ -185,7 +191,10 @@
"my_lots": "My Lots",
"go_home": "Go to Home",
"text1": "Click to enter live room",
"text2": "Beijing Time"
"text2": "Beijing Time",
"text3": "This auction",
"text4": "Not started",
"text5": "Ended"
},
"live_room": {
"error_mess": "Failed to get live content, retry?",
@ -260,7 +269,9 @@
"placeholder": {
"amount": "Maximum {currency}{price}"
},
"amount": "Payment Amount"
"amount": "Payment Amount",
"amountRequired": "Please enter the amount",
"exceedAmount": "Can't be greater than the total amount"
},
"signature": {
"protocol": {
@ -286,7 +297,8 @@
"transfer": "Auction Transfer Confirmation"
},
"error": {
"getRecord": "Failed to get auction record"
"getRecord": "Failed to get auction record",
"incompleteForm": "Please fill in the complete information"
},
"action": {
"agree": "Agree and Sign",
@ -411,7 +423,7 @@
"fullPayment": "Pay in Full",
"partialPayment": "Partial Payment",
"confirmPayment": "Confirm Payment",
"maxAmount": "Maximum {currency}{price}",
"maxAmount": "most",
"enterAmount": "Please enter amount",
"exceedTotal": "Cannot exceed total amount"
},
@ -426,7 +438,8 @@
"message": {
"amountRequired": "Please enter amount",
"lotNoRequired": "Please enter Lot No.",
"deleteSuccess": "Delete successful"
"deleteSuccess": "Delete successful",
"lotNoType": "Lot number format error"
}
},
"validation": {

View File

@ -133,8 +133,8 @@
"otherTab": "中国本土以外の居住者",
"cnTabDesc": "身分証明書の情報を入力してください",
"otherTabDesc": "個人情報をアップロードしてください",
"idCard": "身分証明書番号",
"idCardPlaceholder": "身分証明書番号を入力してください",
"idCard": "ID番号",
"idCardPlaceholder": "ID番号を入力してください",
"name": "名前",
"namePlaceholder": "名前を入力してください",
"gender": "性別",
@ -157,7 +157,13 @@
"male": "男性",
"female": "女性"
}
}
},
"idTye": "ドキュメントタイプ",
"passport": "パスポート",
"other": "他の",
"idNumber": "ID番号",
"idNumberPlaceholder": "ID番号を入力してください",
"idTypeString": "IDカード"
},
"detail": {
"text1": "アーティスト",
@ -185,7 +191,10 @@
"my_lots": "マイ商品",
"go_home": "ホームへ",
"text1": "クリックしてライブルームに入る",
"text2": "北京時間"
"text2": "北京時間",
"text3": "このオークション",
"text4": "開始していません",
"text5": "終了しました"
},
"live_room": {
"error_mess": "ライブコンテンツの取得に失敗しました。再試行しますか?",
@ -260,7 +269,9 @@
"placeholder": {
"amount": "最大 {currency}{price}"
},
"amount": "支払い金額"
"amount": "支払い金額",
"amountRequired": "金額を入力してください",
"exceedAmount": "総額よりも大きくすることはできません"
},
"signature": {
"protocol": {
@ -286,7 +297,8 @@
"transfer": "オークション譲渡確認"
},
"error": {
"getRecord": "オークション記録の取得に失敗しました"
"getRecord": "オークション記録の取得に失敗しました",
"incompleteForm": "完全な情報を入力してください"
},
"action": {
"agree": "同意して署名",
@ -411,7 +423,7 @@
"fullPayment": "全額支払い",
"partialPayment": "一部支払い",
"confirmPayment": "支払い確認",
"maxAmount": "最大 {currency}{price}",
"maxAmount": "ほとんど",
"enterAmount": "金額を入力してください",
"exceedTotal": "合計金額を超えることはできません"
},
@ -426,7 +438,8 @@
"message": {
"amountRequired": "金額を入力してください",
"lotNoRequired": "ロット番号を入力してください",
"deleteSuccess": "削除成功"
"deleteSuccess": "削除成功",
"lotNoType": "ロット番号形式エラー"
}
},
"validation": {
@ -659,4 +672,4 @@
"httpNotInitialized": "HTTPクライアントが初期化されていません。先にsetupHttpを呼び出してください"
}
}
}
}

View File

@ -133,8 +133,8 @@
"otherTab": "非大陆居民",
"cnTabDesc": "请填写身份证相关信息",
"otherTabDesc": "请上传个人相关信息",
"idCard": "身份证号",
"idCardPlaceholder": "请输入身份证号",
"idCard": "号",
"idCardPlaceholder": "请输入号",
"name": "姓名",
"namePlaceholder": "请输入姓名",
"gender": "性别",
@ -157,7 +157,13 @@
"male": "男",
"female": "女"
}
}
},
"idTye": "证件类型",
"passport": "护照",
"other": "其他",
"idNumber": "证件号",
"idNumberPlaceholder": "请输入证件号",
"idTypeString": "身份证"
},
"detail": {
"text1": "作者",
@ -170,7 +176,7 @@
"text8": "拍卖品详情"
},
"art_detail_page": {
"button": "去支付",
"button": "去支付",
"prompt_title": "恭喜您",
"prompt_desc": "竞拍成功"
},
@ -185,7 +191,10 @@
"my_lots": "我的拍品",
"go_home": "去首页",
"text1": "点击进入直播间",
"text2": "北京时间"
"text2": "北京时间",
"text3": "本场拍卖会",
"text4": "未开始",
"text5": "已结束"
},
"live_room": {
"error_mess": "直播内容获取失败,是否重新获取",
@ -196,7 +205,7 @@
"confirm": "确认出价",
"button": "点击'开启出价',即刻参与竞拍",
"start": "开始拍卖",
"head":"领先",
"head": "领先",
"out": "出局",
"success": "成交",
"next_lot": "即将开始下一个拍品",
@ -225,7 +234,7 @@
},
"personal": {
"title": "请填写个人相关信息",
"text":"文本",
"text": "文本",
"next": "下一步"
},
"payment": {
@ -236,7 +245,8 @@
"fail": "支付失败",
"unpaid": "未支付",
"expired": "支付已过期",
"partial": "部分支付"
"partial": "部分支付",
"backToHome": "回到首页"
},
"text1": "支付结果",
"text2": "支付成功",
@ -260,7 +270,9 @@
"placeholder": {
"amount": "最多"
},
"amount": "支付金额"
"amount": "支付金额",
"amountRequired": "请输入金额",
"exceedAmount": "不能大于全部金额"
},
"signature": {
"protocol": {
@ -286,7 +298,8 @@
"transfer": "《拍卖移交确认书》"
},
"error": {
"getRecord": "获取拍卖笔录失败"
"getRecord": "获取拍卖笔录失败",
"incompleteForm": "请填写完整信息"
},
"action": {
"agree": "同意并签字",
@ -413,7 +426,8 @@
"confirmPayment": "确认支付",
"text1": "最多",
"enterAmount": "请输入金额",
"exceedTotal": "不得高于全部金额"
"exceedTotal": "不得高于全部金额",
"maxAmount": "最多"
},
"signature": {
"resultText": "领取您的专属号牌",
@ -427,7 +441,8 @@
"message": {
"amountRequired": "请输入金额",
"lotNoRequired": "请输入Lot号",
"deleteSuccess": "删除成功"
"deleteSuccess": "删除成功",
"lotNoType": "Lot号格式错误"
}
},
"validation": {
@ -660,4 +675,4 @@
"httpNotInitialized": "HTTP客户端未初始化请先调用setupHttp"
}
}
}
}

View File

@ -184,8 +184,8 @@
"otherTab": "非大陸居民",
"cnTabDesc": "請填寫身份證相關信息",
"otherTabDesc": "請上傳個人相關信息",
"idCard": "身份證號",
"idCardPlaceholder": "請輸入身份證號",
"idCard": "號",
"idCardPlaceholder": "請輸入號",
"name": "姓名",
"namePlaceholder": "請輸入姓名",
"gender": "性別",
@ -208,7 +208,13 @@
"male": "男",
"female": "女"
}
}
},
"idTye": "證件類型",
"passport": "護照",
"other": "其他",
"idNumber": "證件號",
"idNumberPlaceholder": "請輸入證件號",
"idTypeString": "身份證"
},
"detail": {
"text1": "作者",
@ -236,7 +242,10 @@
"my_lots": "我的拍品",
"go_home": "去首頁",
"text1": "點擊進入直播間",
"text2": "北京時間"
"text2": "北京時間",
"text3": "本場拍賣會",
"text4": "未開始",
"text5": "已結束"
},
"live_room": {
"error_mess": "直播內容獲取失敗,是否重新獲取",
@ -311,7 +320,9 @@
"placeholder": {
"amount": "最多{currency}{price}"
},
"amount": "支付金額"
"amount": "支付金額",
"amountRequired": "請輸入金額",
"exceedAmount": "不能大於全部金額"
},
"signature": {
"protocol": {
@ -337,7 +348,8 @@
"transfer": "《拍賣移交確認書》"
},
"error": {
"getRecord": "獲取拍賣筆錄失敗"
"getRecord": "獲取拍賣筆錄失敗",
"incompleteForm": "請填寫完整信息"
},
"action": {
"agree": "同意並簽字",
@ -411,7 +423,7 @@
"fullPayment": "支付全部",
"partialPayment": "支付部分",
"confirmPayment": "確認支付",
"maxAmount": "最多{currency}{price}",
"maxAmount": "最多",
"enterAmount": "請輸入金額",
"exceedTotal": "不得高於全部金額"
},
@ -426,7 +438,8 @@
"message": {
"amountRequired": "請輸入金額",
"lotNoRequired": "請輸入Lot號",
"deleteSuccess": "刪除成功"
"deleteSuccess": "刪除成功",
"lotNoType": "Lot號格式錯誤"
}
},
"validation": {
@ -660,4 +673,3 @@
}
}
}

View File

@ -1,7 +1,8 @@
import dotenv from 'dotenv'
import process from 'node:process'
import preload from './app/utils/preload'
import { currentLocales } from './i18n/i18n'
import fs from 'fs'
import path from 'path'
const envFile = process.env.ENV_FILE || '.env.test'
dotenv.config({ path: `./env/${envFile}` })
const publicConfig = Object.entries(process.env)
@ -11,6 +12,19 @@ const publicConfig = Object.entries(process.env)
return config
}, {})
let httpsOptions = {}
try {
// 读取文件并转换为字符串
const key = fs.readFileSync(path.resolve(__dirname, 'ssl/localhost-key.pem'), 'utf-8')
const cert = fs.readFileSync(path.resolve(__dirname, 'ssl/localhost.pem'), 'utf-8')
httpsOptions = { key, cert }
} catch (error) {
// 失败时使用HTTP
httpsOptions = false
}
export default defineNuxtConfig({
modules: [
'@vant/nuxt',
@ -36,20 +50,17 @@ export default defineNuxtConfig({
postcss: {
plugins: {
'autoprefixer': {},
// https://github.com/wswmsword/postcss-mobile-forever
'postcss-mobile-forever': {
appSelector: '#__nuxt',
viewportWidth: 375,
// devtools excluded
exclude: /@nuxt/,
border: true,
rootContainingBlockSelectorList: [
'van-tabbar',
'van-popup',
'van-overlay',
],
},
'postcss-px-to-viewport': {
viewportWidth: 375, // 设计稿宽度
viewportUnit: 'vmin', // 关键配置
fontViewportUnit: 'vmin', // 字体单位
unitPrecision: 5,
propList: ['*'],
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
exclude: /@nuxt/
}
},
},
i18n: {
@ -57,11 +68,10 @@ export default defineNuxtConfig({
lazy: true,
strategy: 'no_prefix',
detectBrowserLanguage: {
useCookie: true,
useCookie: false,
cookieKey: 'i18n_redirected',
redirectOn: 'root',
alwaysRedirect: true,
fallbackLocale: 'zh-CN'
},
defaultLocale: 'zh-CN',
vueI18n: './i18n/i18n.config.ts',
@ -77,6 +87,13 @@ export default defineNuxtConfig({
link: [
{ rel: 'icon', href: '/favicon.ico', sizes: 'any' },
],
// stripe支付CDN引用
script: [
{
src: 'https://js.stripe.com/v3/',
defer: true // 可选,建议添加 defer
}
],
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover,user-scalable=no' },
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
@ -84,9 +101,6 @@ export default defineNuxtConfig({
{ name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#ffffff' },
{ name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#222222' },
],
script: [
{ innerHTML: preload(), type: 'text/javascript', tagPosition: 'head' },
],
},
},
nitro: {
@ -135,9 +149,10 @@ export default defineNuxtConfig({
compatibilityVersion: 4,
},
// 指定 Nuxt 应用程序的兼容性日期,确保应用程序在未来的 Nuxt 版本中保持稳定性
compatibilityDate: '2025-01-09',
compatibilityDate: '2025-02-28',
devServer: {
host: '0.0.0.0', // Set the host to 'localhost'
port: 3000, // Set the port to 3000 or any other port you prefer
// https: httpsOptions,
host: '0.0.0.0',
port: 3000,
},
})

View File

@ -23,7 +23,6 @@
"@vueuse/core": "^12.4.0",
"aliyun-aliplayer": "^2.28.5",
"axios": "^1.7.9",
"countup.js": "^2.8.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
@ -48,7 +47,7 @@
"bumpp": "^9.9.2",
"cross-env": "^7.0.3",
"ipx": "^3.0.1",
"postcss-mobile-forever": "^4.3.1",
"postcss-px-to-viewport": "^1.1.1",
"sass": "^1.83.1",
"sass-loader": "^16.0.4",
"sharp": "^0.33.5",

View File

@ -29,9 +29,6 @@ importers:
axios:
specifier: ^1.7.9
version: 1.7.9
countup.js:
specifier: ^2.8.0
version: 2.8.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
@ -99,9 +96,9 @@ importers:
ipx:
specifier: ^3.0.1
version: 3.0.1(db0@0.2.4)(ioredis@5.5.0)
postcss-mobile-forever:
specifier: ^4.3.1
version: 4.3.2(postcss@8.5.2)
postcss-px-to-viewport:
specifier: ^1.1.1
version: 1.1.1
sass:
specifier: ^1.83.1
version: 1.85.0
@ -2099,9 +2096,6 @@ packages:
typescript:
optional: true
countup.js@2.8.0:
resolution: {integrity: sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==}
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
@ -3610,11 +3604,6 @@ packages:
peerDependencies:
postcss: ^8.4.31
postcss-mobile-forever@4.3.2:
resolution: {integrity: sha512-l1YuvxouJ7wt2awKtomuTDKXkNlFcUvC62sVUD9+Gr2AdyZRTtP81izo/IHfS2fbbFCY5JzvuTGmAJ2SyEqv+w==}
peerDependencies:
postcss: ^8.0.0
postcss-normalize-charset@7.0.0:
resolution: {integrity: sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==}
engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
@ -3675,6 +3664,9 @@ packages:
peerDependencies:
postcss: ^8.4.31
postcss-px-to-viewport@1.1.1:
resolution: {integrity: sha512-2x9oGnBms+e0cYtBJOZdlwrFg/mLR4P1g2IFu7jYKvnqnH/HLhoKyareW2Q/x4sg0BgklHlP1qeWo2oCyPm8FQ==}
postcss-reduce-initial@7.0.2:
resolution: {integrity: sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==}
engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
@ -7017,8 +7009,6 @@ snapshots:
typescript: 5.7.3
optional: true
countup.js@2.8.0: {}
crc-32@1.2.2: {}
crc32-stream@6.0.0:
@ -8757,10 +8747,6 @@ snapshots:
postcss: 8.5.2
postcss-selector-parser: 6.1.2
postcss-mobile-forever@4.3.2(postcss@8.5.2):
dependencies:
postcss: 8.5.2
postcss-normalize-charset@7.0.0(postcss@8.5.2):
dependencies:
postcss: 8.5.2
@ -8812,6 +8798,11 @@ snapshots:
postcss: 8.5.2
postcss-value-parser: 4.2.0
postcss-px-to-viewport@1.1.1:
dependencies:
object-assign: 4.1.1
postcss: 8.5.2
postcss-reduce-initial@7.0.2(postcss@8.5.2):
dependencies:
browserslist: 4.24.4

242
public/stripe/checkout.css Normal file
View File

@ -0,0 +1,242 @@
/* Variables */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 16px;
-webkit-font-smoothing: antialiased;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
height: 100vh;
width: 100vw;
}
form {
width: 30vw;
min-width: 500px;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
margin-top: auto;
margin-bottom: auto;
}
.hidden {
display: none;
}
#payment-message {
color: rgb(105, 115, 134);
font-size: 16px;
line-height: 20px;
padding-top: 12px;
text-align: center;
}
#payment-element {
margin-bottom: 24px;
}
/* Buttons and links */
button {
background: #0055DE;
font-family: Arial, sans-serif;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: block;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
button:hover {
filter: contrast(115%);
}
button:disabled {
opacity: 0.5;
cursor: default;
}
/* spinner/processing state, errors */
.spinner,
.spinner:before,
.spinner:after {
border-radius: 50%;
}
.spinner {
color: #ffffff;
font-size: 22px;
text-indent: -99999px;
margin: 0px auto;
position: relative;
width: 20px;
height: 20px;
box-shadow: inset 0 0 0 2px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before,
.spinner:after {
position: absolute;
content: "";
}
.spinner:before {
width: 10.4px;
height: 20.4px;
background: #0055DE;
border-radius: 20.4px 0 0 20.4px;
top: -0.2px;
left: -0.2px;
-webkit-transform-origin: 10.4px 10.2px;
transform-origin: 10.4px 10.2px;
-webkit-animation: loading 2s infinite ease 1.5s;
animation: loading 2s infinite ease 1.5s;
}
.spinner:after {
width: 10.4px;
height: 10.2px;
background: #0055DE;
border-radius: 0 10.2px 10.2px 0;
top: -0.1px;
left: 10.2px;
-webkit-transform-origin: 0px 10.2px;
transform-origin: 0px 10.2px;
-webkit-animation: loading 2s infinite ease;
animation: loading 2s infinite ease;
}
/* Payment status page */
#payment-status {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
row-gap: 30px;
width: 30vw;
min-width: 500px;
min-height: 380px;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
opacity: 0;
animation: fadeInAnimation 1s ease forwards;
}
#status-icon {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
width: 40px;
border-radius: 50%;
}
h2 {
margin: 0;
color: #30313D;
text-align: center;
}
a {
text-decoration: none;
font-size: 16px;
font-weight: 600;
font-family: Arial, sans-serif;
display: block;
}
a:hover {
filter: contrast(120%);
}
#details-table {
overflow-x: auto;
width: 100%;
}
table {
width: 100%;
font-size: 14px;
border-collapse: collapse;
}
table tbody tr:first-child td {
border-top: 1px solid #E6E6E6; /* Top border */
padding-top: 10px;
}
table tbody tr:last-child td {
border-bottom: 1px solid #E6E6E6; /* Bottom border */
}
td {
padding-bottom: 10px;
}
.TableContent {
text-align: right;
color: #6D6E78;
}
.TableLabel {
font-weight: 600;
color: #30313D;
}
#view-details {
color: #0055DE;
}
#retry-button {
text-align: center;
background: #0055DE;
color: #ffffff;
border-radius: 4px;
border: 0;
padding: 12px 16px;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
@-webkit-keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes fadeInAnimation {
to {
opacity: 1;
}
}
@media only screen and (max-width: 600px) {
form, #payment-status{
width: 80vw;
min-width: initial;
}
}

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Accept a payment</title>
<meta name="description" content="A demo of a payment on Stripe" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="checkout.css" />
<script src="https://js.stripe.com/v3/"></script>
<script src="checkout.js" defer></script>
</head>
<body>
<!-- Display a payment form -->
<form id="payment-form">
<div id="payment-element">
<!--Stripe.js injects the Payment Element-->
</div>
<button id="submit">
<div class="spinner hidden" id="spinner"></div>
<span id="button-text">Pay now</span>
</button>
<div id="payment-message" class="hidden"></div>
</form>
</body>
</html>

89
public/stripe/checkout.js Normal file
View File

@ -0,0 +1,89 @@
// This is your test publishable API key.
const stripe = Stripe("pk_test_51QfbSAAB1Vm8VfJq3AWsR4k2mZjnlF7XFrmlbc6XVXrtwXquAUfwzZmOFDbxMIAwqJBgqao8KLt2wmPc4vNOCTeo00WB78KtfV");
// The items the customer wants to buy
const items = [{ id: "xl-tshirt", amount: 1000 }];
let elements;
initialize();
document
.querySelector("#payment-form")
.addEventListener("submit", handleSubmit);
// Fetches a payment intent and captures the client secret
async function initialize() {
// const response = await fetch("/create-payment-intent", {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify({ items }),
// });
// const { clientSecret } = await response.json();
const clientSecret='pi_3QxII1AB1Vm8VfJq1OyR3bkz_secret_d8fgL53X6T3MQpYfi2lRH3V1F'
const appearance = {
theme: 'stripe',
};
elements = stripe.elements({ appearance, clientSecret });
const paymentElementOptions = {
layout: "accordion",
};
const paymentElement = elements.create("payment", paymentElementOptions);
paymentElement.mount("#payment-element");
}
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: "http://localhost:4242/complete.html",
},
});
// This point will only be reached if there is an immediate error when
// confirming the payment. Otherwise, your customer will be redirected to
// your `return_url`. For some payment methods like iDEAL, your customer will
// be redirected to an intermediate site first to authorize the payment, then
// redirected to the `return_url`.
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("An unexpected error occurred.");
}
setLoading(false);
}
// ------- UI helpers -------
function showMessage(messageText) {
const messageContainer = document.querySelector("#payment-message");
messageContainer.classList.remove("hidden");
messageContainer.textContent = messageText;
setTimeout(function () {
messageContainer.classList.add("hidden");
messageContainer.textContent = "";
}, 4000);
}
// Show a spinner on payment submission
function setLoading(isLoading) {
if (isLoading) {
// Disable the button and show a spinner
document.querySelector("#submit").disabled = true;
document.querySelector("#spinner").classList.remove("hidden");
document.querySelector("#button-text").classList.add("hidden");
} else {
document.querySelector("#submit").disabled = false;
document.querySelector("#spinner").classList.add("hidden");
document.querySelector("#button-text").classList.remove("hidden");
}
}

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Order Status</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="checkout.css" />
<script src="https://js.stripe.com/v3/"></script>
<script src="complete.js" defer></script>
</head>
<body>
<!-- Display the order status -->
<div id="payment-status">
<div id="status-icon"></div>
<h2 id="status-text"></h2>
<div id="details-table">
<table>
<tbody>
<tr>
<td class="TableLabel">id</td>
<td id="intent-id" class="TableContent"></td>
</tr>
<tr>
<td class="TableLabel">status</td>
<td id="intent-status" class="TableContent"></td>
</tr>
</tbody>
</table>
</div>
<a href="#" id="view-details" rel="noopener noreferrer" target="_blank">View details
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 3.49998C2.64175 3.49998 2.25 3.89173 2.25 4.37498V11.375C2.25 11.8582 2.64175 12.25 3.125 12.25H10.125C10.6082 12.25 11 11.8582 11 11.375V9.62498C11 9.14173 11.3918 8.74998 11.875 8.74998C12.3582 8.74998 12.75 9.14173 12.75 9.62498V11.375C12.75 12.8247 11.5747 14 10.125 14H3.125C1.67525 14 0.5 12.8247 0.5 11.375V4.37498C0.5 2.92524 1.67525 1.74998 3.125 1.74998H4.875C5.35825 1.74998 5.75 2.14173 5.75 2.62498C5.75 3.10823 5.35825 3.49998 4.875 3.49998H3.125Z" fill="#0055DE"/> <path d="M8.66672 0C8.18347 0 7.79172 0.391751 7.79172 0.875C7.79172 1.35825 8.18347 1.75 8.66672 1.75H11.5126L4.83967 8.42295C4.49796 8.76466 4.49796 9.31868 4.83967 9.66039C5.18138 10.0021 5.7354 10.0021 6.07711 9.66039L12.7501 2.98744V5.83333C12.7501 6.31658 13.1418 6.70833 13.6251 6.70833C14.1083 6.70833 14.5001 6.31658 14.5001 5.83333V0.875C14.5001 0.391751 14.1083 0 13.6251 0H8.66672Z" fill="#0055DE"/></svg>
</a>
<a id="retry-button" href="/checkout.html">Test another</a>
</div>
</body>
</html>

85
public/stripe/complete.js Normal file
View File

@ -0,0 +1,85 @@
// ------- UI Resources -------
const SuccessIcon =
`<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4695 0.232963C15.8241 0.561287 15.8454 1.1149 15.5171 1.46949L6.14206 11.5945C5.97228 11.7778 5.73221 11.8799 5.48237 11.8748C5.23253 11.8698 4.99677 11.7582 4.83452 11.5681L0.459523 6.44311C0.145767 6.07557 0.18937 5.52327 0.556912 5.20951C0.924454 4.89575 1.47676 4.93936 1.79051 5.3069L5.52658 9.68343L14.233 0.280522C14.5613 -0.0740672 15.1149 -0.0953599 15.4695 0.232963Z" fill="white"/>
</svg>`;
const ErrorIcon =
`<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.25628 1.25628C1.59799 0.914573 2.15201 0.914573 2.49372 1.25628L8 6.76256L13.5063 1.25628C13.848 0.914573 14.402 0.914573 14.7437 1.25628C15.0854 1.59799 15.0854 2.15201 14.7437 2.49372L9.23744 8L14.7437 13.5063C15.0854 13.848 15.0854 14.402 14.7437 14.7437C14.402 15.0854 13.848 15.0854 13.5063 14.7437L8 9.23744L2.49372 14.7437C2.15201 15.0854 1.59799 15.0854 1.25628 14.7437C0.914573 14.402 0.914573 13.848 1.25628 13.5063L6.76256 8L1.25628 2.49372C0.914573 2.15201 0.914573 1.59799 1.25628 1.25628Z" fill="white"/>
</svg>`;
const InfoIcon =
`<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 1.5H4C2.61929 1.5 1.5 2.61929 1.5 4V10C1.5 11.3807 2.61929 12.5 4 12.5H10C11.3807 12.5 12.5 11.3807 12.5 10V4C12.5 2.61929 11.3807 1.5 10 1.5ZM4 0C1.79086 0 0 1.79086 0 4V10C0 12.2091 1.79086 14 4 14H10C12.2091 14 14 12.2091 14 10V4C14 1.79086 12.2091 0 10 0H4Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 7C5.25 6.58579 5.58579 6.25 6 6.25H7.25C7.66421 6.25 8 6.58579 8 7V10.5C8 10.9142 7.66421 11.25 7.25 11.25C6.83579 11.25 6.5 10.9142 6.5 10.5V7.75H6C5.58579 7.75 5.25 7.41421 5.25 7Z" fill="white"/>
<path d="M5.75 4C5.75 3.31075 6.31075 2.75 7 2.75C7.68925 2.75 8.25 3.31075 8.25 4C8.25 4.68925 7.68925 5.25 7 5.25C6.31075 5.25 5.75 4.68925 5.75 4Z" fill="white"/>
</svg>`;
// ------- UI helpers -------
function setPaymentDetails(intent) {
let statusText = "Something went wrong, please try again.";
let iconColor = "#DF1B41";
let icon = ErrorIcon;
if (!intent) {
setErrorState();
return;
}
switch (intent.status) {
case "succeeded":
statusText = "Payment succeeded";
iconColor = "#30B130";
icon = SuccessIcon;
break;
case "processing":
statusText = "Your payment is processing.";
iconColor = "#6D6E78";
icon = InfoIcon;
break;
case "requires_payment_method":
statusText = "Your payment was not successful, please try again.";
break;
default:
break;
}
document.querySelector("#status-icon").style.backgroundColor = iconColor;
document.querySelector("#status-icon").innerHTML = icon;
document.querySelector("#status-text").textContent= statusText;
document.querySelector("#intent-id").textContent = intent.id;
document.querySelector("#intent-status").textContent = intent.status;
document.querySelector("#view-details").href = `https://dashboard.stripe.com/payments/${intent.id}`;
}
function setErrorState() {
document.querySelector("#status-icon").style.backgroundColor = "#DF1B41";
document.querySelector("#status-icon").innerHTML = ErrorIcon;
document.querySelector("#status-text").textContent= "Something went wrong, please try again.";
document.querySelector("#details-table").classList.add("hidden");
document.querySelector("#view-details").classList.add("hidden");
}
// Stripe.js instance
const stripe = Stripe("pk_test_51QfbSAAB1Vm8VfJq3AWsR4k2mZjnlF7XFrmlbc6XVXrtwXquAUfwzZmOFDbxMIAwqJBgqao8KLt2wmPc4vNOCTeo00WB78KtfV");
checkStatus();
// Fetches the payment intent status after payment submission
async function checkStatus() {
const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);
if (!clientSecret) {
setErrorState();
return;
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
setPaymentDetails(paymentIntent);
}

View File

@ -0,0 +1,25 @@
export default defineEventHandler(async (event) => {
const url = getRequestURL(event)
// 只处理 create-payment-intent 请求
if (url.pathname === '/create-payment-intent' && event.method === 'POST') {
try {
const body = await readBody(event)
const { items } = body
// 计算总金额
const amount = items.reduce((total: number, item: any) => total + item.amount, 0)
// 模拟创建支付意向的响应
// 注意clientSecret 格式应该类似于 'pi_xxxxx_secret_xxxxx'
return {
clientSecret: `pi_${Math.random().toString(36).substring(2)}_secret_${Math.random().toString(36).substring(2)}`
}
} catch (error) {
throw createError({
statusCode: 500,
message: '创建支付意向失败'
})
}
}
})

View File

@ -1,3 +0,0 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

28
ssl/localhost-key.pem Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFtE2AWs1o0X/A
0+506WAG49xbO3KJQTWKwHl5pfeP0GieVjV3qKXkHXl9Fyhg9oJnCAz2nzxzavAP
W9NevDbX7KdEiZQjDhpgO+V9IGUqqJKEQnpVk57G3fHBX40nQ4zAQ/S1Vj4Aw9Tk
iqNFgLRX3HTAT0NO3eXENJbsDdGxtOuLn5h+DWThrqCaUK2Xnkee3eG/VwgKJKVH
LWuC8xyb1NydtchaRFBjoO9oT6Tb/MGeBOrNH9e7Ndh9mCFepXUhzNxrN1fKjn9s
107pSEeZb4il2K7GqE0W6r6Y3eeBR2+mEBRqeFnZObT0JkBLI2QIrAwOnFO6ObQC
ZaEjwHcZAgMBAAECggEASMZB8Ql7qyXS3OwmTqrJSj/+ESck1hlG2DhZfsn1At84
Y3BgZheSWRHwcndfybFz9vEjtHSRD/tBOqYWfDzUA099kuEBwpWiZ+Ika5bNJpK+
vCisV2vrelCgeQnvL5DR8sQRA98nG6j6aNYPm7nwqJbh8xg6MoHD3iFtnJ7JnZvQ
pSa6Z9qq4Po+cp63/U3yEzFeiVVDTMQJMVClANUCX3jLHs8B85WMbb1eKKFe/xCA
n2BWlFVI7Hld+hxhKWkc71+kafC5hUz1w88FcBaN2W/DAtJgKC0dHYATwLCUGC4Z
CoCZfB7b3JOzK4mGJ/XxxUcBUk+oweExOrYwCfW4MQKBgQDJbTM4qgW2ca/Xc7cp
zKvclgtkJ8rWbVZqMYW6fpXoOdhhxjJSx+LfeGk1whz3Xj04EdSZbRI6zYVaHfHa
HFkA2Na/Wi9hboid4WVXUi5RXzthVTOYi1jAJNmK4R25wuSdcGVoYxSrSLyDcHbx
MF2cFdQ4A386L+RcoDYzImWCXwKBgQD7RO4DZJgkqDng6YFrYqIdQkJn6keXl4yW
Hq1FGaa7XDjlun/X2jT0xJJPFcwLQLbWwrkwmUYN/VQEbYwYlyB0MegF9VflfILl
/leCXC8/9WEknQkPqu8N1JiYhajIKLxfQX35nW/oK+S5prJOBxNw+3Of/S43R5Xe
60EEI9iphwKBgQCrs/Sn5vd7sKnOpYuLjDcskJMhS3JzGz1AxPpUIbgz/6tenY8k
VdQl3wUAmHoMvD6/XyO1re6ORcfZLBGQdf3A5RcagwxEp+65dvvmVd256844iGK1
NIPxNvhilMe8JFCxjLBFLcDeyeA4w1QBAdOqTEldfk2kElM+SiwppraVTQKBgCUP
O5OgiJgPf8neZsox1/s8xJKTCVAgeAnEKIYijGbh6Tpo0WZCtsDLJVEow9l9B/qQ
6cNzN9PkYznr9lfCInVAzxnh377nKF9Hrhx6ADYMuPEvgCChc3S0wHTuccBj0bSy
8iOYxuKVZrzDC1Va0dE+JQWZz/EzS7V/OS2lI9WNAoGBAKujoPDn36/hJ/Zr8XAM
CEbOi0q0N7I37aRKO8Wm55SCGDYWtBlu+NiIMqk3gzgomtm/cVF+fUNv0BOKc+hx
x6PQE98AEn5LdGeqLpDY66vhyR8WGUyCBPB+Dn8OFFT+njL2E8NcQi0kS3t/YlR/
oobyxGhm4M1fM8HtGwqQJX60
-----END PRIVATE KEY-----

26
ssl/localhost.pem Normal file
View File

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEbjCCAtagAwIBAgIRAJjJaJNy+AO5JPuasTslJMQwDQYJKoZIhvcNAQELBQAw
gZ8xHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE6MDgGA1UECwwxREVT
S1RPUC1DOTVCMVIzXDM3MzYzQERFU0tUT1AtQzk1QjFSMyAo6YKi6Zuo5p2oKTFB
MD8GA1UEAww4bWtjZXJ0IERFU0tUT1AtQzk1QjFSM1wzNzM2M0BERVNLVE9QLUM5
NUIxUjMgKOmCoumbqOadqCkwHhcNMjUwMzAyMDIzMzM3WhcNMjcwNjAyMDIzMzM3
WjBlMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxOjA4
BgNVBAsMMURFU0tUT1AtQzk1QjFSM1wzNzM2M0BERVNLVE9QLUM5NUIxUjMgKOmC
oumbqOadqCkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFtE2AWs1o
0X/A0+506WAG49xbO3KJQTWKwHl5pfeP0GieVjV3qKXkHXl9Fyhg9oJnCAz2nzxz
avAPW9NevDbX7KdEiZQjDhpgO+V9IGUqqJKEQnpVk57G3fHBX40nQ4zAQ/S1Vj4A
w9TkiqNFgLRX3HTAT0NO3eXENJbsDdGxtOuLn5h+DWThrqCaUK2Xnkee3eG/VwgK
JKVHLWuC8xyb1NydtchaRFBjoO9oT6Tb/MGeBOrNH9e7Ndh9mCFepXUhzNxrN1fK
jn9s107pSEeZb4il2K7GqE0W6r6Y3eeBR2+mEBRqeFnZObT0JkBLI2QIrAwOnFO6
ObQCZaEjwHcZAgMBAAGjXjBcMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr
BgEFBQcDATAfBgNVHSMEGDAWgBQVJpbTujNcXUH/91CnxLerp/gKbDAUBgNVHREE
DTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggGBAERAae9YDQgjnVtDQUWL
kIbMowvN6BospgO2srV+aXCDLbB22jnq4cGsTpVjxo80Nl6M0iSRz29K+jy4YFsL
efTOeks1EpVQB/UnYuo391p5wzevXwa3s7dH7Oc+917y8JDiLNnSVEct+tk4zeOZ
QbVzx6Gexiii7k1uSG/G1NYrRiXf3ggM93Fyu5NM+u8CzZvWm46ix9reYimVqfPa
VjHsiQnmKbh+CD6iDWm9y1jUxqBay4cAbo2AVxIvBDdsC9KSCTsbP4hBPx9foy1U
cLRxUGsWTVPPS2BmP8o6CSa2tNPeVNCWSP89tanY2mzGErfVXLV8t5E4awF0ea+a
kbjyG3svVC6/rLo8LpFPonr4mQWfGcFntmGUC314d5z1ZCCS5ENEWAGZ3b3XzPsU
Yh2QQnt4gtvWaTRqwqhSL9DLFp106/tok3hq8MyDFcxTxWKyDZsgaieoRGnF11EW
tdIqnK9nwVOyAzaO603SuEoMiGBpb9nj/cAFsvm56YUVrg==
-----END CERTIFICATE-----