deotalandAi/apps/frontend/src/components/PurchaseModal/index.vue

1488 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div v-if="show" class="purchase-overlay" @click="onClose">
<div class="purchase-container" @click.stop>
<!-- 支付中蒙层 -->
<div v-if="showPayingOverlay" class="paying-overlay">
<div class="paying-content">
<div class="paying-spinner"></div>
<div class="paying-text">{{ 'PayLoading' }}</div>
</div>
</div>
<button class="close-button" @click="onClose" aria-label="关闭">
<el-icon class="close-icon"><CloseBold /></el-icon>
</button>
<div class="blog-layout">
<!-- Hero Section -->
<section class="hero-section">
<div class="hero-image">
<img :src="props.modelData.imageUrl" alt="Custom Models" />
</div>
<div class="product-info">
<h1 class="product-title">{{ $t('checkout.customModel') }}</h1>
<div class="price-info">
<span class="price">{{ $t('checkout.from') }} {{unt=='USD'?'$':unt}} {{ (amountCents ).toFixed(2) }}</span>
</div>
</div>
</section>
<!-- Content Sections -->
<div class="content-sections">
<div class="main-content">
<!-- 产品配置区域 -->
<div class="content-block">
<h2 class="block-title">
{{ $t('checkout.configuration') }}
</h2>
<div class="config-grid">
<div class="config-item quantity">
<div class="label">{{ $t('checkout.quantity') }}</div>
<div class="qty-container">
<button class="qty-btn" @click="decQty"></button>
<div class="qty-val">{{ qty }}</div>
<button class="qty-btn" @click="incQty"></button>
</div>
</div>
<div class="config-item ip-name">
<div class="label">{{ $t('checkout.ipName') }}</div>
<el-input
v-model="ipName"
:placeholder="$t('checkout.ipNamePlaceholder')"
class="ip-name-input"
/>
</div>
<div class="config-item shop-select">
<div class="label">{{ $t('checkout.shop') }}</div>
<el-select
v-model="selectedShop"
:placeholder="$t('checkout.chooseShop')"
filterable
:loading="loadingShops"
class="shop-select-input"
clearable
value-key="id"
>
<el-option
v-for="shop in shopList"
:key="shop.id"
:label="shop.shopName"
:value="shop"
/>
</el-select>
</div>
</div>
</div>
<!-- 优惠券选择区域 -->
<div class="content-block">
<h2 class="block-title">
{{ $t('checkout.voucher') }}
</h2>
<div class="voucher-section">
<div v-if="loadingVouchers" class="loading-vouchers">
<div class="loading-spinner"></div>
<span>{{ $t('checkout.loading') }}</span>
</div>
<div v-else-if="voucherList.length === 0" class="no-vouchers">
<span>{{ $t('checkout.noVouchers') }}</span>
</div>
<div v-else class="voucher-list">
<div
v-for="voucher in voucherList"
:key="voucher.id"
class="voucher-item"
:class="{ 'selected': selectedVoucher?.id === voucher.id }"
@click="onVoucherSelect(voucher)"
>
<div class="voucher-left">
<div class="voucher-amount">
{{ voucher.currency === 'USD' ? '$' : voucher.currency }}{{ voucher.amount }}
</div>
<div class="voucher-code">{{ voucher.couponCode }}</div>
</div>
<div class="voucher-right">
<div class="voucher-desc">{{ voucher.sourceDesc }}</div>
<div class="voucher-min">{{ $t('checkout.minOrder') }}: {{ voucher.currency === 'USD' ? '$' : voucher.currency }}{{ voucher.minOrderAmount }}</div>
<div class="voucher-expire">{{ $t('checkout.expireAt') }}: {{ formatDate(voucher.expireAt) }}</div>
</div>
<div class="voucher-check">
<div class="check-icon" v-if="selectedVoucher?.id === voucher.id">✓</div>
</div>
</div>
</div>
</div>
</div>
<!-- 联系信息和配送信息并排布局 -->
<div class="info-row">
<!-- 联系信息 -->
<div class="info-column">
<h2 class="block-title">
{{ $t('checkout.contact') }}
</h2>
<el-form class="contact-form">
<el-form-item :label="$t('checkout.emailOrPhone')">
<el-input v-model="contact.emailOrPhone" :placeholder="$t('checkout.emailOrPhone')" />
</el-form-item>
<el-form-item>
<el-checkbox v-model="contact.subscribe">{{ $t('checkout.subscribe') }}</el-checkbox>
</el-form-item>
</el-form>
<!-- 流程说明 -->
<div class="process">
<h3 class="process-title">{{ $t('checkout.processTitle') || '我们的流程如下' }}</h3>
<ul class="process-list">
<li>{{ $t('checkout.orderConfirmation') }}</li>
<!-- <li>{{ $t('checkout.productionTime') }}</li> -->
<li>{{ $t('checkout.logistics') }}</li>
<li>{{ $t('checkout.afterSales') }}</li>
</ul>
</div>
</div>
<!-- 配送信息 -->
<div class="info-column">
<h2 class="block-title">
{{ $t('checkout.shipping') }}
</h2>
<el-form :model="shipping" label-width="auto" class="shipping-form">
<div class="form-row">
<el-form-item :label="$t('checkout.country')">
<el-select v-model="shipping.country" :placeholder="$t('checkout.chooseCountry')" filterable>
<el-option v-for="c in countryOptions" :key="c.value" :label="c.label" :value="c.value" />
</el-select>
</el-form-item>
</div>
<div class="form-row">
<el-form-item :label="$t('checkout.lastName')">
<el-input v-model="shipping.lastName" :placeholder="$t('checkout.lastName')" />
</el-form-item>
<el-form-item :label="$t('checkout.firstName')">
<el-input v-model="shipping.firstName" :placeholder="$t('checkout.firstName')" />
</el-form-item>
</div>
<div class="form-row">
<el-form-item :label="$t('checkout.postalCode')">
<el-input v-model="shipping.postalCode" :placeholder="`${$t('checkout.postalCode')} (${$t('common.optional')})`">
<template #suffix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('checkout.state')">
<el-select v-model="shipping.state" :placeholder="$t('checkout.chooseState')">
<el-option v-for="s in stateOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
</div>
<div class="form-row">
<el-form-item :label="$t('checkout.city')" class="full-width">
<el-input v-model="shipping.city" :placeholder="$t('checkout.city')" />
</el-form-item>
</div>
<div class="form-row">
<el-form-item :label="$t('checkout.address1')" class="full-width">
<el-input v-model="shipping.address1" :placeholder="$t('checkout.address1')" />
</el-form-item>
</div>
<div class="form-row">
<el-form-item :label="$t('checkout.address2')" class="full-width">
<el-input v-model="shipping.address2" :placeholder="$t('checkout.address2')" />
</el-form-item>
</div>
<div class="form-row">
<el-form-item :label="$t('checkout.phone')" class="full-width">
<el-input v-model="shipping.phone" :placeholder="$t('checkout.phone')" />
</el-form-item>
</div>
<el-form-item>
<el-checkbox v-model="shipping.saveInfo">{{ $t('checkout.saveInfo') }}</el-checkbox>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-area">
<button class="buy-btn" :disabled="isPayButtonDisabled" @click="goShopify">{{ $t('checkout.buy') }}</button>
</div>
<!-- Stripe Payment Overlay -->
<div v-if="showStripe" class="stripe-overlay" @click="onStripeCancel">
<div class="stripe-card" @click.stop>
<StripePaymentForm
:amount="amountCents"
:currency="'usd'"
:orderId="props.id || String(Date.now())"
:customerEmail="contact.emailOrPhone || ''"
@payment-success="onStripeSuccess"
@payment-error="onStripeError"
@cancel="onStripeCancel"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { ElIcon } from 'element-plus'
import { CloseBold, Search } from '@element-plus/icons-vue'
import StripePaymentForm from '@/components/StripePaymentForm.vue'
import { Country, State } from 'country-state-city'
import { useI18n } from 'vue-i18n'
import { PayServer } from '@deotaland/utils'
import { requestUtils,clientApi } from '@deotaland/utils'
import { PurchaseModal as PurchaseModalClass } from './index.js'
const payserver = new PayServer();
const purchaseModal = new PurchaseModalClass();
const props = defineProps({
modelData: { type: Object, default: () => ({}) },
show: { type: Boolean, default: false },
series: { type: String, default: '' }
})
const emit = defineEmits(['close'])
const onClose = () => emit('close')
const addons = ref({ matte:false, gloss:false, base:false })
const qty = ref(1)
const ipName = ref('')
const contact = ref({ emailOrPhone:'', subscribe:false })
const shipping = ref({ country:'US', firstName:'', lastName:'', postalCode:'', state:'', city:'', address1:'', address2:'', phone:'', saveInfo:false })
const countryOptions = ref([])
const stateOptions = ref([])
const showStripe = ref(false)
const { locale } = useI18n()
const showPayingOverlay = ref(false)
const voucherList = ref([])
const selectedVoucher = ref(null)
const loadingVouchers = ref(false)
const discount_amount= ref(0);
// 省州映射数据已移至国际化文件
const amountCents = computed(() => {
let base = price.value // 默认价格,移除了尺寸相关定价
const total = (base * qty.value) - discount_amount.value
return total
})
// 店铺选择相关状态
const shopList = ref([])
const selectedShop = ref(null)
const loadingShops = ref(false)
// 计算支付按钮是否禁用
const isPayButtonDisabled = computed(() => {
return !(
shipping.value.firstName.trim() &&
shipping.value.lastName.trim() &&
shipping.value.state.trim() &&
shipping.value.city.trim() &&
shipping.value.address1.trim() &&
shipping.value.phone.trim() &&
contact.value.emailOrPhone.trim() &&
ipName.value.trim()
)
})
const unt = ref('');
const price = ref(0);
const seriesId = ref('');
const getVoucherList = async () => {
loadingVouchers.value = true
try {
const res = await requestUtils.common(clientApi.default.getAvailableCoupon)
if (res.code === 0) {
voucherList.value = res.data || []
}
} catch (error) {
console.error('获取优惠券列表失败:', error)
voucherList.value = []
} finally {
loadingVouchers.value = false
}
}
const onVoucherSelect = (voucher) => {
if (selectedVoucher.value?.id === voucher.id) {
selectedVoucher.value = null
} else {
selectedVoucher.value = voucher
}
updatePayInfo();
}
//更新支付信息
const updatePayInfo = () => {
let parmas = {
currency:unt.value,
amount:price.value,
}
if(selectedVoucher.value){
parmas.coupon_ids = [selectedVoucher.value.id]
}else{
parmas.coupon_ids = []
}
requestUtils.common(clientApi.default.calculateUnitAmount,parmas).then(res => {
const data = res.data;
discount_amount.value = data.discount_amount || 0
})
}
const formatDate = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
//获取对应价格
const getPrice = async () => {
const res = await requestUtils.common(clientApi.default.getProductList)
if(res.code === 0){
const data = res.data.list || []
const item = data.find(item => item.name === props.series)
if(item){
price.value = item.price?.amount || 0
unt.value = item.price?.currency || ''
seriesId.value = item.id || ''
}
}
}
// 获取店铺列表
const fetchShopList = async () => {
loadingShops.value = true
try {
const res = await purchaseModal.getShopList()
if (res.code === 0) {
shopList.value = res.data || []
}
} catch (error) {
console.error('获取店铺列表失败:', error)
shopList.value = []
} finally {
loadingShops.value = false
}
}
const incQty = () => { qty.value = Math.min(qty.value+1, 99) }
const decQty = () => { qty.value = Math.max(qty.value-1, 1) }
const goShopify = () => {//用户点击购买
const project_details = {
imageUrl:props.modelData.imageUrl,
modelUrl:props.modelData.modelUrl,
projectId:props.modelData.projectId,
}
// 整理用户输入的信息
const order_info = {
quantity: qty.value,
ipName: ipName.value,
contact: {
emailOrPhone: contact.value.emailOrPhone
},
shipping: {
country: shipping.value.country,
countryLabel: countryOptions.value.find(c => c.value === shipping.value.country)?.label || shipping.value.country,
firstName: shipping.value.firstName,
lastName: shipping.value.lastName,
postalCode: shipping.value.postalCode,
state: shipping.value.state,
stateLabel: stateOptions.value.find(s => s.value === shipping.value.state)?.label || shipping.value.state,
city: shipping.value.city,
address1: shipping.value.address1,
address2: shipping.value.address2,
phone: shipping.value.phone
},
modelData:project_details
}
let params ={
quantity:qty.value,
project_id:props.modelData.projectId,
project_details:project_details,
order_info:order_info,
}
if(selectedVoucher.value){
params.coupon_ids = [selectedVoucher.value.id]
}else{
params.coupon_ids = []
}
if(selectedShop.value){
params.shop_id = selectedShop.value.id
}
// 显示支付中蒙层
showPayingOverlay.value = true
// 5秒后隐藏蒙层
setTimeout(() => {
showPayingOverlay.value = false
}, 5000)
// Save shipping and contact information if checkbox is checked
saveLocal()
// 在控制台打印整理后的信息
params.product_id = seriesId.value
payserver.createPayorOrder(params);
}
const saveLocal = () => {
try {
if (shipping.value.saveInfo) {
localStorage.setItem('purchase_shipping', JSON.stringify(shipping.value))
}
localStorage.setItem('purchase_contact', JSON.stringify(contact.value))
} catch (e) {}
}
const getCountryLabel = (code, name) => {
try {
const dn = new Intl.DisplayNames([locale.value], { type: 'region' })
return dn.of(code) || name
} catch (e) {
return name
}
}
const updateCountryOptions = () => {
const allCountries = (Country.getAllCountries() || []).map(c => ({ label: getCountryLabel(c.isoCode, c.name), value: c.isoCode }))
// 将中国选项置顶
const chinaOption = allCountries.find(c => c.value === 'CN')
if (chinaOption) {
countryOptions.value = [chinaOption, ...allCountries.filter(c => c.value !== 'CN')]
} else {
countryOptions.value = allCountries
}
}
onMounted(() => {
try {
const s = localStorage.getItem('purchase_shipping')
if (s) Object.assign(shipping.value, JSON.parse(s))
const c = localStorage.getItem('purchase_contact')
if (c) Object.assign(contact.value, JSON.parse(c))
updateCountryOptions()
updateStates()
getPrice();
getVoucherList();
fetchShopList();
} catch (e) {}
})
watch(() => shipping.value.country, () => { updateStates() })
watch(() => locale.value, (newLocale, oldLocale) => {
console.log('Language changed from', oldLocale, 'to', newLocale)
updateCountryOptions()
updateStates()
})
const onStripeSuccess = () => {
saveLocal()
showStripe.value = false
onClose()
}
const onStripeError = () => {
}
const onStripeCancel = () => {
showStripe.value = false
}
const updateStates = () => {
const list = State.getStatesOfCountry(shipping.value.country) || []
stateOptions.value = list.map(s => {
let label = s.name
if (shipping.value.country === 'CN') {
const code = s.isoCode?.startsWith('CN-') ? s.isoCode : `CN-${s.isoCode}`
// 使用国际化系统的省份映射
const provinceMap = (locale.value === 'zh' ?
// 中文映射
{
'CN-BJ': '北京市',
'CN-SH': '上海市',
'CN-TJ': '天津市',
'CN-CQ': '重庆市',
'CN-HE': '河北省',
'CN-SX': '山西省',
'CN-NM': '内蒙古自治区',
'CN-LN': '辽宁省',
'CN-JL': '吉林省',
'CN-HL': '黑龙江省',
'CN-JS': '江苏省',
'CN-ZJ': '浙江省',
'CN-AH': '安徽省',
'CN-FJ': '福建省',
'CN-JX': '江西省',
'CN-SD': '山东省',
'CN-HA': '河南省',
'CN-HB': '湖北省',
'CN-HN': '湖南省',
'CN-GD': '广东省',
'CN-GX': '广西壮族自治区',
'CN-HI': '海南省',
'CN-SC': '四川省',
'CN-GZ': '贵州省',
'CN-YN': '云南省',
'CN-XZ': '西藏自治区',
'CN-SN': '陕西省',
'CN-GS': '甘肃省',
'CN-QH': '青海省',
'CN-NX': '宁夏回族自治区',
'CN-XJ': '新疆维吾尔自治区',
'CN-TW': '台湾省',
'CN-HK': '香港特别行政区',
'CN-MO': '澳门特别行政区'
} :
// 英文映射
{
'CN-BJ': 'Beijing',
'CN-SH': 'Shanghai',
'CN-TJ': 'Tianjin',
'CN-CQ': 'Chongqing',
'CN-HE': 'Hebei',
'CN-SX': 'Shanxi',
'CN-NM': 'Inner Mongolia',
'CN-LN': 'Liaoning',
'CN-JL': 'Jilin',
'CN-HL': 'Heilongjiang',
'CN-JS': 'Jiangsu',
'CN-ZJ': 'Zhejiang',
'CN-AH': 'Anhui',
'CN-FJ': 'Fujian',
'CN-JX': 'Jiangxi',
'CN-SD': 'Shandong',
'CN-HA': 'Henan',
'CN-HB': 'Hubei',
'CN-HN': 'Hunan',
'CN-GD': 'Guangdong',
'CN-GX': 'Guangxi Zhuang Autonomous Region',
'CN-HI': 'Hainan',
'CN-SC': 'Sichuan',
'CN-GZ': 'Guizhou',
'CN-YN': 'Yunnan',
'CN-XZ': 'Tibet Autonomous Region',
'CN-SN': 'Shaanxi',
'CN-GS': 'Gansu',
'CN-QH': 'Qinghai',
'CN-NX': 'Ningxia Hui Autonomous Region',
'CN-XJ': 'Xinjiang Uygur Autonomous Region',
'CN-TW': 'Taiwan',
'CN-HK': 'Hong Kong SAR',
'CN-MO': 'Macau SAR'
})
label = provinceMap[code] || s.name
}
return { label, value: s.isoCode }
})
// Set the first state as default if no state is selected
if (stateOptions.value.length > 0 && !shipping.value.state) {
shipping.value.state = stateOptions.value[0].value
}
}
</script>
<style scoped>
/* Blog Layout Styles */
.purchase-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(6px);
z-index: 1002; display: flex; align-items: center; justify-content: center;
}
.purchase-container {
width: min(1200px, 95vw); height: min(90vh, 800px);
background: var(--content-bg,#111827);
border: 1px solid var(--border-color,#374151);
border-radius: 16px; color: var(--text-color,#f9fafb);
box-shadow: 0 25px 80px rgba(0,0,0,0.45);
overflow: hidden; position: relative;
display: flex; flex-direction: column;
}
.close-button {
position: absolute; top: 16px; right: 16px; width: 40px; height: 40px;
border-radius: 10px; border: 1px solid rgba(255,255,255,0.22);
background: rgba(17,24,39,0.6); color: #fff;
display: inline-flex; align-items: center; justify-content: center;
cursor: pointer; z-index: 10; transition: all 0.3s ease;
}
.close-button:hover {
background: rgba(139,92,246,0.3);
border-color: rgba(139,92,246,0.6);
transform: scale(1.05);
}
.close-icon { color: #ffffff; font-size: 18px; }
.blog-layout {
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
}
/* Hero Section */
.hero-section {
display: flex;
align-items: center;
padding: 32px;
background: linear-gradient(135deg, rgba(139,92,246,0.1) 0%, rgba(59,130,246,0.1) 100%);
border-bottom: 1px solid rgba(255,255,255,0.1);
gap: 24px;
}
.hero-image {
flex-shrink: 0;
width: 120px;
height: 120px;
border-radius: 16px;
overflow: hidden;
border: 2px solid rgba(139,92,246,0.3);
background: rgba(11,13,18,0.8);
}
.hero-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.product-title {
margin: 0;
font-size: 32px;
font-weight: 700;
color: #ffffff;
line-height: 1.2;
background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.price-info {
display: flex;
align-items: center;
gap: 12px;
}
.price {
font-size: 16px;
color: #e5e7eb;
font-weight: 500;
}
.price-highlight {
color: #10b981;
font-weight: 600;
font-size: 18px;
}
/* Content Sections Layout */
.content-sections {
padding: 24px;
flex: 1;
overflow-y: auto;
}
.main-content {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Content Blocks */
.content-block {
background: rgba(17,24,39,0.8);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 24px;
transition: all 0.3s ease;
}
.content-block:hover {
border-color: rgba(139,92,246,0.3);
box-shadow: 0 4px 20px rgba(139,92,246,0.1);
}
.block-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: #ffffff;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.title-icon {
color: #8b5cf6;
font-size: 20px;
}
/* Configuration Grid */
.config-grid {
display: grid;
grid-template-columns: 200px 1fr 1fr;
gap: 24px;
align-items: start;
}
.config-item {
display: flex;
flex-direction: column;
gap: 12px;
}
.config-item.quantity {
align-items: flex-start;
}
.config-item.ip-name {
align-items: flex-start;
}
.config-item.shop-select {
align-items: flex-start;
}
.ip-name-input {
width: 100%;
max-width: 300px;
}
.shop-select-input {
width: 100%;
max-width: 300px;
}
.ip-name-input :deep(.el-input__wrapper),
.shop-select-input :deep(.el-select__wrapper) {
background: rgba(17,24,39,0.8);
border-color: rgba(255,255,255,0.2);
color: #ffffff;
border-radius: 8px;
}
.ip-name-input :deep(.el-input__inner),
.shop-select-input :deep(.el-select__placeholder) {
color: #ffffff;
}
.shop-select-input :deep(.el-select__popper) {
background: rgba(17,24,39,0.95);
border-color: rgba(255,255,255,0.2);
color: #ffffff;
}
.shop-select-input :deep(.el-select__popper .el-select-dropdown__item) {
color: #ffffff;
}
.shop-select-input :deep(.el-select__popper .el-select-dropdown__item:hover) {
background: rgba(139,92,246,0.2);
}
.shop-select-input :deep(.el-select__popper .el-select-dropdown__item.selected) {
background: rgba(139,92,246,0.3);
}
/* Voucher Section */
.voucher-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.loading-vouchers,
.no-vouchers {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: #9ca3af;
font-size: 14px;
gap: 12px;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(139,92,246,0.3);
border-top-color: #8b5cf6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.voucher-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 300px;
overflow-y: auto;
}
.voucher-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: rgba(17,24,39,0.6);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.voucher-item:hover {
border-color: rgba(139,92,246,0.4);
background: rgba(139,92,246,0.05);
}
.voucher-item.selected {
border-color: #8b5cf6;
background: rgba(139,92,246,0.1);
}
.voucher-left {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 80px;
}
.voucher-amount {
font-size: 24px;
font-weight: 700;
color: #10b981;
line-height: 1;
}
.voucher-code {
font-size: 12px;
color: #9ca3af;
font-family: monospace;
}
.voucher-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.voucher-desc {
font-size: 14px;
color: #e5e7eb;
font-weight: 500;
}
.voucher-min,
.voucher-expire {
font-size: 12px;
color: #9ca3af;
}
.voucher-check {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(139,92,246,0.2);
border: 2px solid #8b5cf6;
}
.check-icon {
color: #8b5cf6;
font-size: 14px;
font-weight: bold;
}
/* Info Row Layout */
.info-row {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 24px;
}
.info-column {
background: rgba(17,24,39,0.6);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
}
.info-column:hover {
border-color: rgba(139,92,246,0.2);
}
/* Contact Form */
.contact-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.contact-form :deep(.el-form-item__label) {
color: #e5e7eb;
font-weight: 500;
}
.contact-form :deep(.el-input__wrapper),
.contact-form :deep(.el-textarea__inner),
.contact-form :deep(.el-select__wrapper) {
background: rgba(17,24,39,0.8);
border-color: rgba(255,255,255,0.2);
color: #ffffff;
border-radius: 8px;
}
.contact-form :deep(.el-input__inner),
.contact-form :deep(.el-textarea__inner) {
color: #ffffff;
}
.contact-form :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #8b5cf6;
border-color: #8b5cf6;
}
.contact-form :deep(.el-checkbox__label) {
color: #e5e7eb;
}
/* Process Styles */
.process {
margin-top: 20px;
background: rgba(139,92,246,0.08);
border: 1px solid rgba(139,92,246,0.2);
border-radius: 10px;
padding: 16px;
transition: all 0.3s ease;
}
.process:hover {
border-color: rgba(139,92,246,0.3);
background: rgba(139,92,246,0.12);
}
.process-title {
font-size: 16px;
font-weight: 600;
color: #8b5cf6;
margin: 0 0 12px 0;
display: flex;
align-items: center;
gap: 8px;
}
.process-title::before {
content: "";
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: rgba(139,92,246,0.2);
color: #8b5cf6;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
.process-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.process-list li {
position: relative;
padding-left: 20px;
font-size: 14px;
line-height: 1.5;
color: #e5e7eb;
transition: color 0.2s ease;
}
.process-list li::before {
content: "✓";
position: absolute;
left: 0;
top: 0;
color: #10b981;
font-weight: bold;
font-size: 12px;
}
.process-list li:hover {
color: #ffffff;
}
.process-list li:nth-child(1)::before { content: "1"; background: #3b82f6; color: white; }
.process-list li:nth-child(2)::before { content: "2"; background: #8b5cf6; color: white; }
.process-list li:nth-child(3)::before { content: "3"; background: #06b6d4; color: white; }
.process-list li:nth-child(4)::before { content: "4"; background: #10b981; color: white; }
.process-list li:nth-child(1)::before,
.process-list li:nth-child(2)::before,
.process-list li:nth-child(3)::before,
.process-list li:nth-child(4)::before {
position: absolute;
left: 0;
top: 0;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
transform: translateY(2px);
}
/* Shipping Form */
.shipping-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.form-row .full-width {
grid-column: 1 / -1;
}
.shipping-form :deep(.el-form-item__label) {
color: #e5e7eb;
font-weight: 500;
font-size: 13px;
}
.shipping-form :deep(.el-input__wrapper),
.shipping-form :deep(.el-textarea__inner),
.shipping-form :deep(.el-select__wrapper) {
background: rgba(17,24,39,0.8);
border-color: rgba(255,255,255,0.2);
color: #ffffff;
border-radius: 6px;
height: 40px;
}
.shipping-form :deep(.el-input__inner),
.shipping-form :deep(.el-textarea__inner) {
color: #ffffff;
font-size: 14px;
}
.shipping-form :deep(.el-select__selected-item) {
color: #ffffff;
}
.shipping-form :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #8b5cf6;
border-color: #8b5cf6;
}
.shipping-form :deep(.el-checkbox__label) {
color: #e5e7eb;
}
.shipping-form :deep(.el-form-item) {
margin-bottom: 12px;
}
/* Option Groups and Controls */
.option-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.label {
font-size: 14px;
color: #e5e7eb;
font-weight: 500;
}
.chip-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
height: 36px;
padding: 0 16px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 20px;
background: rgba(31,41,55,0.9);
color: #ffffff;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
font-weight: 500;
}
.chip:hover {
border-color: rgba(139,92,246,0.6);
background: rgba(139,92,246,0.2);
transform: translateY(-1px);
}
.chip.active {
background: rgba(139,92,246,0.35);
border-color: rgba(139,92,246,0.8);
box-shadow: 0 2px 8px rgba(139,92,246,0.3);
}
/* Quantity Control */
.qty-container {
display: inline-flex;
align-items: center;
gap: 0;
background: rgba(17,24,39,0.9);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
overflow: hidden;
}
.qty-btn {
width: 40px;
height: 40px;
border: none;
background: rgba(31,41,55,0.9);
color: #ffffff;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.qty-btn:hover {
background: rgba(139,92,246,0.3);
color: #ffffff;
}
.qty-btn:first-child {
border-right: 1px solid rgba(255,255,255,0.1);
}
.qty-btn:last-child {
border-left: 1px solid rgba(255,255,255,0.1);
}
.qty-val {
width: 60px;
text-align: center;
font-weight: 700;
color: #ffffff;
font-size: 16px;
background: rgba(17,24,39,0.8);
}
/* Action Area */
.action-area {
padding: 24px 32px;
background: rgba(17,24,39,0.95);
border-top: 1px solid rgba(255,255,255,0.1);
display: flex;
justify-content: center;
}
.buy-btn {
height: 48px;
padding: 0 32px;
border-radius: 12px;
border: 1px solid rgba(139,92,246,0.6);
background: linear-gradient(135deg, rgba(139,92,246,0.8), rgba(59,130,246,0.8));
color: #fff;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
min-width: 160px;
}
.buy-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(139,92,246,0.4);
background: linear-gradient(135deg, rgba(139,92,246,1), rgba(59,130,246,1));
}
.buy-btn:active {
transform: translateY(0);
}
.buy-btn:disabled {
opacity: 0.6;
background: linear-gradient(135deg, rgba(75,85,99,0.8), rgba(55,65,81,0.8));
border-color: rgba(75,85,99,0.6);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.buy-btn:disabled:hover {
transform: none;
box-shadow: none;
background: linear-gradient(135deg, rgba(75,85,99,0.8), rgba(55,65,81,0.8));
}
/* Stripe Payment Overlay */
.stripe-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
z-index: 1003;
display: flex;
align-items: center;
justify-content: center;
}
.stripe-card {
width: min(560px, 92vw);
max-height: 80vh;
overflow-y: auto;
background: #f9fafb;
border: 1px solid var(--border-color,#374151);
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.35);
}
/* Responsive Design */
@media (max-width: 1024px) {
.content-sections {
padding: 20px;
}
.config-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.ip-name-input,
.shop-select-input {
max-width: 100%;
}
.info-row {
grid-template-columns: 1fr;
gap: 20px;
}
.form-row {
grid-template-columns: 1fr;
gap: 8px;
}
.hero-section {
padding: 24px;
gap: 20px;
}
.hero-image {
width: 100px;
height: 100px;
}
.product-title {
font-size: 28px;
}
.action-area {
padding: 20px 24px;
}
}
@media (max-width: 768px) {
.purchase-container {
width: 100vw;
height: 100vh;
border-radius: 0;
}
.content-sections {
padding: 16px;
}
.content-block {
padding: 16px;
}
.info-column {
padding: 16px;
}
.hero-section {
padding: 20px;
flex-direction: column;
text-align: center;
gap: 16px;
}
.hero-image {
width: 80px;
height: 80px;
}
.product-title {
font-size: 24px;
}
.buy-btn {
width: 100%;
padding: 0 24px;
}
}
@media (max-width: 480px) {
.content-sections {
padding: 12px;
}
.content-block {
padding: 12px;
}
.block-title {
font-size: 16px;
margin-bottom: 16px;
}
.chip-group {
gap: 6px;
}
.chip {
height: 32px;
padding: 0 12px;
font-size: 12px;
}
.qty-container {
transform: scale(0.9);
}
.hero-section {
padding: 16px;
}
.action-area {
padding: 16px;
}
}
/* 支付中蒙层样式 */
.paying-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 1004;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
}
.paying-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 40px;
background: rgba(17, 24, 39, 0.95);
border: 1px solid rgba(139, 92, 246, 0.3);
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.paying-spinner {
width: 48px;
height: 48px;
border: 4px solid rgba(139, 92, 246, 0.2);
border-top: 4px solid #8b5cf6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.paying-text {
font-size: 18px;
font-weight: 600;
color: #ffffff;
text-align: center;
line-height: 1.4;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 - 支付蒙层 */
@media (max-width: 768px) {
.paying-content {
padding: 30px 20px;
margin: 20px;
}
.paying-spinner {
width: 40px;
height: 40px;
}
.paying-text {
font-size: 16px;
}
}
@media (max-width: 480px) {
.paying-content {
padding: 24px 16px;
margin: 16px;
}
.paying-spinner {
width: 36px;
height: 36px;
}
.paying-text {
font-size: 14px;
}
}
</style>