fix:首页充值
This commit is contained in:
@@ -1,109 +1,282 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { createReusableTemplate } from '@vueuse/core';
|
import { useBillingStore } from '@/store/modules/billing/billing';
|
||||||
import { $t } from '@/locales';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useFormRules } from '@/hooks/common/form';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
defineOptions({
|
const { t } = useI18n();
|
||||||
name: 'CardData'
|
const { patternRules } = useFormRules();
|
||||||
});
|
|
||||||
|
|
||||||
interface CardData {
|
interface RechargeOption {
|
||||||
key: string;
|
amount: number;
|
||||||
title: string;
|
displayAmount: string;
|
||||||
value: number;
|
price: number;
|
||||||
unit: string;
|
selected?: boolean;
|
||||||
color: {
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
};
|
|
||||||
icon: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardData = computed<CardData[]>(() => [
|
const emailAddress = ref('');
|
||||||
{
|
const emailError = ref('');
|
||||||
key: 'visitCount',
|
const customAmount = ref<string | number | undefined>(undefined);
|
||||||
title: $t('page.home.visitCount'),
|
const selectedAmount = ref<number | null>(null);
|
||||||
value: 9725,
|
const isCustomMode = ref<boolean>(false);
|
||||||
unit: '',
|
|
||||||
color: {
|
const rechargeOptions: Ref<RechargeOption[]> = ref([
|
||||||
start: '#ec4786',
|
{ amount: 10, displayAmount: '10元', price: 10.00 },
|
||||||
end: '#b955a4'
|
{ amount: 20, displayAmount: '20元', price: 20.00 },
|
||||||
},
|
{ amount: 30, displayAmount: '30元', price: 30.00 },
|
||||||
icon: 'ant-design:bar-chart-outlined'
|
{ amount: 50, displayAmount: '50元', price: 50.00 },
|
||||||
},
|
{ amount: 100, displayAmount: '100元', price: 100.00 },
|
||||||
{
|
{ amount: 200, displayAmount: '200元', price: 200.00 },
|
||||||
key: 'turnover',
|
|
||||||
title: $t('page.home.turnover'),
|
|
||||||
value: 1026,
|
|
||||||
unit: '$',
|
|
||||||
color: {
|
|
||||||
start: '#865ec0',
|
|
||||||
end: '#5144b4'
|
|
||||||
},
|
|
||||||
icon: 'ant-design:money-collect-outlined'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'downloadCount',
|
|
||||||
title: $t('page.home.downloadCount'),
|
|
||||||
value: 970925,
|
|
||||||
unit: '',
|
|
||||||
color: {
|
|
||||||
start: '#56cdf3',
|
|
||||||
end: '#719de3'
|
|
||||||
},
|
|
||||||
icon: 'carbon:document-download'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'dealCount',
|
|
||||||
title: $t('page.home.dealCount'),
|
|
||||||
value: 9527,
|
|
||||||
unit: '',
|
|
||||||
color: {
|
|
||||||
start: '#fcbc25',
|
|
||||||
end: '#f68057'
|
|
||||||
},
|
|
||||||
icon: 'ant-design:trademark-circle-outlined'
|
|
||||||
}
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
interface GradientBgProps {
|
const billingStore = useBillingStore();
|
||||||
gradientColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>();
|
const paymentAmount = computed(() => {
|
||||||
|
if (customAmount.value !== undefined && customAmount.value !== '') {
|
||||||
|
return Number(customAmount.value);
|
||||||
|
}
|
||||||
|
return selectedAmount.value || 0;
|
||||||
|
});
|
||||||
|
|
||||||
function getGradientColor(color: CardData['color']) {
|
const handleOptionSelect = (amount: number) => {
|
||||||
return `linear-gradient(to bottom right, ${color.start}, ${color.end})`;
|
isCustomMode.value = false;
|
||||||
}
|
selectedAmount.value = amount;
|
||||||
|
customAmount.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomMode = () => {
|
||||||
|
isCustomMode.value = true;
|
||||||
|
selectedAmount.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomAmount = (value: string | number | undefined) => {
|
||||||
|
customAmount.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加点击外部关闭自定义输入的处理
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (isCustomMode.value &&
|
||||||
|
!target?.closest('.special-option') &&
|
||||||
|
customAmount.value === undefined) {
|
||||||
|
isCustomMode.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建一个函数来处理 blur 事件
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (customAmount.value === undefined || customAmount.value === '') {
|
||||||
|
isCustomMode.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
const balance = computed(() => billingStore.balance);
|
||||||
|
|
||||||
|
// 创建邮箱验证规则,将 trigger 改为 'blur'
|
||||||
|
const emailRules = computed<Rule[]>(() => [
|
||||||
|
{ required: true, message: t('page.login.register.emailRequired'), trigger: 'blur' },
|
||||||
|
patternRules.email // 预定义规则已经设置为 blur 触发
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 移除 change 事件处理,改用 blur 事件处理
|
||||||
|
const handleEmailBlur = async (e: FocusEvent) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
emailAddress.value = target.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用验证规则进行验证
|
||||||
|
await Promise.all(emailRules.value.map(rule => {
|
||||||
|
if (rule.validator) {
|
||||||
|
return rule.validator(rule, target.value, () => {});
|
||||||
|
}
|
||||||
|
if (rule.pattern) {
|
||||||
|
if (!rule.pattern.test(target.value)) {
|
||||||
|
throw rule.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}));
|
||||||
|
emailError.value = '';
|
||||||
|
} catch (error) {
|
||||||
|
emailError.value = error as string;
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ACard :bordered="false" size="small" class="card-wrapper">
|
<div class="recharge-container p-4">
|
||||||
<!-- define component start: GradientBg -->
|
<!-- 邮箱输入部分 -->
|
||||||
<DefineGradientBg v-slot="{ $slots, gradientColor }">
|
<div class="email-section mb-6">
|
||||||
<div class="rd-8px px-16px pb-4px pt-8px text-white" :style="{ backgroundImage: gradientColor }">
|
<div class="text-lg font-bold mb-2">{{ t('page.carddata.email') }}</div>
|
||||||
<component :is="$slots.default" />
|
<div class="email-input-wrapper">
|
||||||
|
<AInput
|
||||||
|
v-model:value="emailAddress"
|
||||||
|
:placeholder="t('page.login.common.emailPlaceholder')"
|
||||||
|
class="email-input"
|
||||||
|
size="large"
|
||||||
|
:status="emailError ? 'error' : ''"
|
||||||
|
@blur="handleEmailBlur"
|
||||||
|
/>
|
||||||
|
<div v-if="emailError" class="error-message">
|
||||||
|
<span class="text-red-500">{{ emailError }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DefineGradientBg>
|
</div>
|
||||||
<!-- define component end: GradientBg -->
|
|
||||||
|
|
||||||
<ARow :gutter="[16, 16]">
|
<!-- 充值金额选择 -->
|
||||||
<ACol v-for="item in cardData" :key="item.key" :span="24" :md="12" :lg="6">
|
<div class="amount-section mb-6">
|
||||||
<GradientBg :gradient-color="getGradientColor(item.color)" class="flex-1">
|
<div class="text-lg font-bold mb-2">{{ t('page.carddata.Rechargeamount') }}</div>
|
||||||
<h3 class="text-16px">{{ item.title }}</h3>
|
<div class="grid grid-cols-3 gap-4">
|
||||||
<div class="flex justify-between pt-12px">
|
<div
|
||||||
<SvgIcon :icon="item.icon" class="text-32px" />
|
v-for="option in rechargeOptions"
|
||||||
<CountTo
|
:key="option.amount"
|
||||||
:prefix="item.unit"
|
class="recharge-option cursor-pointer"
|
||||||
:start-value="1"
|
:class="{ 'selected': selectedAmount === option.amount }"
|
||||||
:end-value="item.value"
|
@click="handleOptionSelect(option.amount)"
|
||||||
class="text-30px text-white dark:text-dark"
|
>
|
||||||
|
<div class="text-lg font-medium">{{ option.displayAmount }}</div>
|
||||||
|
<div class="text-sm text-gray-500">{{ t('page.carddata.price') }} {{ option.price.toFixed(2) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义充选项 -->
|
||||||
|
<div
|
||||||
|
class="special-option"
|
||||||
|
:class="{ 'selected': isCustomMode }"
|
||||||
|
@click="handleCustomMode"
|
||||||
|
>
|
||||||
|
<template v-if="!isCustomMode">
|
||||||
|
<AInputNumber
|
||||||
|
v-model:value="customAmount"
|
||||||
|
:placeholder="t('page.carddata.Customization')"
|
||||||
|
:min="1"
|
||||||
|
:max="999999"
|
||||||
|
class="w-full"
|
||||||
|
@change="handleCustomAmount"
|
||||||
|
@click.stop
|
||||||
|
@blur="handleBlur"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
</GradientBg>
|
</div>
|
||||||
</ACol>
|
</div>
|
||||||
</ARow>
|
</div>
|
||||||
</ACard>
|
|
||||||
|
<!-- 支付部分 -->
|
||||||
|
<div class="payment-section mt-8 bg-white p-4 rounded-lg border border-gray-100">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<span class="text-gray-600">{{ t('page.carddata.Remainingbalance') }}:{{ balance }}元</span>
|
||||||
|
<span class="text-blue-500">{{ t('page.carddata.Theamountreceived') }}:{{ paymentAmount.toFixed(2) }}元</span>
|
||||||
|
</div>
|
||||||
|
<AButton
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
:disabled="!paymentAmount"
|
||||||
|
>
|
||||||
|
¥{{ paymentAmount.toFixed(2) }} {{ t('page.carddata.pay') }}
|
||||||
|
</AButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.recharge-option {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
height: 76px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-option.selected,
|
||||||
|
.special-option.selected {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-option {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
transition: all 0.3s;
|
||||||
|
height: 76px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-option :deep(.ant-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-option :deep(.ant-input-number-input-wrap) {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-option :deep(.ant-input-number-input) {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
&::placeholder {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-option :deep(.ant-input-number:focus),
|
||||||
|
.special-option :deep(.ant-input-number-focused) {
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-section {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-input {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-left: 4px;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除之前的后缀样式 */
|
||||||
|
:deep(.ant-input-suffix) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user