front/src/views/user/recharge/deposit.vue
zzh 25445bc146 feat: 完善日夜间主题与揽收下单体验
统一暗色模式页面/顶栏/Handsontable 选中色,调整下单到仓方式文案与提示,CustomLeftMenu 卡片统一 60px;补充揽收单、问题件处理与 H5 相关页面。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 11:49:23 +08:00

591 lines
15 KiB
Vue

<template>
<div class="deposit-page animate__animated animate__fadeInUp animate__faster">
<div class="deposit-layout">
<section class="info-panel">
<div class="panel-header">
<p class="panel-subtitle">{{ $t('user.recharge.deposit.panel.subtitle') }}</p>
<h2>{{ $t('user.recharge.deposit.panel.title') }}</h2>
<p class="panel-desc">{{ $t('user.recharge.deposit.panel.desc') }}</p>
</div>
<a-steps :current="current" direction="vertical" class="steps">
<a-step :title="$t('user.recharge.deposit.steps.selectAmount.title')" :description="$t('user.recharge.deposit.steps.selectAmount.desc')" />
<a-step :title="$t('user.recharge.deposit.steps.uploadVoucher.title')" :description="$t('user.recharge.deposit.steps.uploadVoucher.desc')" />
<a-step :title="$t('user.recharge.deposit.steps.review.title')" :description="$t('user.recharge.deposit.steps.review.desc')" />
</a-steps>
<div class="tips-card">
<p class="tips-title">{{ $t('user.recharge.deposit.tips.title') }}</p>
<ul>
<li>{{ $t('user.recharge.deposit.tips.item1') }}</li>
<li>{{ $t('user.recharge.deposit.tips.item2') }}</li>
<li>{{ $t('user.recharge.deposit.tips.item3') }}</li>
</ul>
</div>
</section>
<section class="form-panel">
<div class="form-header">
<div class="amount-box">
<p class="amount-label">{{ $t('user.recharge.deposit.form.currentAmount') }}</p>
<p class="amount-value">¥{{ finalAmount || '--' }}</p>
</div>
<span class="step-chip">{{ $t('user.recharge.deposit.form.stepChip', { step: current + 1, tip: currentTip }) }}</span>
</div>
<a-form :model="formModel">
<div v-if="current === 0" class="form-block">
<p class="block-title">{{ $t('user.recharge.deposit.form.selectAmount') }}</p>
<a-radio-group
v-model="rechargeAmount"
type="button"
button-style="solid"
class="amount-buttons"
>
<a-radio :value="500">¥500</a-radio>
<a-radio :value="1000">¥1,000</a-radio>
<a-radio :value="1500">¥1,500</a-radio>
<a-radio :value="3000">¥3,000</a-radio>
<a-radio :value="0">{{ $t('user.recharge.deposit.form.custom') }}</a-radio>
</a-radio-group>
<div
v-if="rechargeAmount === 0"
class="custom-amount animate__animated animate__fadeIn"
>
<label>{{ $t('user.recharge.deposit.form.inputAmount') }}</label>
<a-input-number
v-model="customAmount"
:min="1"
:precision="2"
style="width: 240px"
>
<template #prefix>¥</template>
<template #suffix>RMB</template>
</a-input-number>
</div>
</div>
<div v-if="current === 0" class="form-block">
<p class="block-title">{{ $t('user.recharge.deposit.form.paymentMethod') }}</p>
<div class="pay-type-grid">
<button
type="button"
class="pay-card"
:class="{ active: payType === 'zfb' }"
@click="payType = 'zfb'"
>
<img :src="zfbIcon" :alt="$t('user.recharge.deposit.payment.alipay')" />
<div>
<p class="pay-name">{{ $t('user.recharge.deposit.payment.alipay') }}</p>
<p class="pay-desc">{{ $t('user.recharge.deposit.payment.alipayDesc') }}</p>
</div>
</button>
<button
type="button"
class="pay-card"
:class="{ active: payType === 'wx' }"
@click="payType = 'wx'"
>
<img :src="wxIcon" :alt="$t('user.recharge.deposit.payment.wechat')" />
<div>
<p class="pay-name">{{ $t('user.recharge.deposit.payment.wechat') }}</p>
<p class="pay-desc">{{ $t('user.recharge.deposit.payment.wechatDesc') }}</p>
</div>
</button>
</div>
<div class="qr-wrapper">
<div class="qr-image" v-if="payType === 'zfb'">
<a-image :src="zfbQrCode" :preview="true" width="220" />
</div>
<div class="qr-image" v-else>
<a-image :src="wxQrCode" :preview="true" width="220" />
</div>
<p class="qr-tip">{{ $t('user.recharge.deposit.form.qrTip') }}</p>
</div>
</div>
<div v-if="current === 1" class="form-block">
<p class="block-title">{{ $t('user.recharge.deposit.form.uploadVoucher') }}</p>
<p class="block-desc">{{ $t('user.recharge.deposit.form.uploadVoucherDesc') }}</p>
<a-upload
name="rechargeScreenshot"
:file-list="fileList"
list-type="picture-card"
:show-upload-list="true"
accept="image/*"
:custom-request="handleUpload"
@change="handleFileChange"
>
<template #upload-button>
<div v-if="!previewUrl" class="upload-card">
<icon-plus />
<span>{{ $t('user.recharge.deposit.form.uploadScreenshot') }}</span>
</div>
</template>
</a-upload>
<div v-if="previewUrl" class="preview-wrap">
<img :src="previewUrl" alt="screenshot" class="preview-image" />
</div>
</div>
<div v-if="current === 2" class="form-block">
<a-result status="success" :title="$t('user.recharge.deposit.result.title')" :sub-title="$t('user.recharge.deposit.result.subtitle')">
<template #extra>
<a-space>
<a-button @click="reset">{{ $t('user.recharge.deposit.result.actions.rechargeAgain') }}</a-button>
<a-button type="primary" @click="goToRecord">{{ $t('user.recharge.deposit.result.actions.viewBill') }}</a-button>
</a-space>
</template>
</a-result>
</div>
<div class="action-row">
<a-button v-if="current === 1" @click="current--">{{ $t('user.recharge.deposit.actions.prevStep') }}</a-button>
<a-button
v-if="current === 1"
type="primary"
:loading="submitting"
@click="submit"
>
{{ $t('user.recharge.deposit.actions.submitReview') }}
</a-button>
<a-button v-if="current === 0" type="primary" @click="handleNext">
{{ $t('user.recharge.deposit.actions.nextStep') }}
</a-button>
</div>
</a-form>
</section>
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { computed, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { IconPlus } from '@arco-design/web-vue/es/icon';
import zfbIcon from '@/assets/zfb.png';
import wxIcon from '@/assets/wx.png';
import zfbQrCode from '@/assets/zfb-biubuy.png';
import wxQrCode from '@/assets/wx-biubuy.png';
import { submitRechargeApplication } from '@/api/recharge/rechargeRecord';
const { t } = useI18n();
defineOptions({
name: 'UserRechargeDeposit',
});
const router = useRouter();
const current = ref(0);
const formModel = reactive({});
const rechargeAmount = ref(500);
const customAmount = ref(0);
const payType = ref<'zfb' | 'wx'>('zfb');
const fileList = ref<any[]>([]);
const screenshotFile = ref<File | null>(null);
const previewUrl = ref('');
const submitting = ref(false);
const finalAmount = computed(() => {
return rechargeAmount.value === 0 ? customAmount.value : rechargeAmount.value;
});
const currentTip = computed(() => {
const tips = [
t('user.recharge.deposit.steps.tip.selectAmount'),
t('user.recharge.deposit.steps.tip.uploadVoucher'),
t('user.recharge.deposit.steps.tip.review'),
];
return tips[current.value] || t('user.recharge.deposit.steps.tip.complete');
});
const handleNext = () => {
if (rechargeAmount.value === 0 && customAmount.value <= 0) {
Message.warning(t('user.recharge.deposit.message.selectAmount'));
return;
}
current.value++;
};
const handleUpload = async (options: any) => {
const file = options?.fileItem?.file ?? options?.file;
if (!file) {
Message.error(t('user.recharge.deposit.message.uploadFailed'));
options?.onError?.(new Error('no file'));
return;
}
if (previewUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl.value);
}
screenshotFile.value = file as File;
previewUrl.value = URL.createObjectURL(file as File);
fileList.value = [{ uid: String(Date.now()), name: (file as File).name, url: previewUrl.value }];
options?.onSuccess?.(file);
Message.success(t('user.recharge.deposit.message.uploadSuccess'));
};
const handleFileChange = (files: any) => {
fileList.value = files;
if (!files?.length) {
screenshotFile.value = null;
if (previewUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl.value);
}
previewUrl.value = '';
}
};
const submit = async () => {
if (rechargeAmount.value === 0 && customAmount.value <= 0) {
Message.warning(t('user.recharge.deposit.message.selectAmount'));
return;
}
if (finalAmount.value < 0.01) {
Message.warning(t('user.recharge.deposit.message.selectAmount'));
return;
}
submitting.value = true;
try {
await submitRechargeApplication({
rechargeAmount: Number(finalAmount.value),
payType: payType.value === 'zfb' ? 2 : 1,
screenshotFile: screenshotFile.value ?? undefined,
});
Message.success(t('user.recharge.deposit.message.submitSuccess'));
current.value++;
} catch (error: any) {
Message.error(error?.message || t('user.recharge.deposit.message.submitFailed'));
} finally {
submitting.value = false;
}
};
const reset = () => {
current.value = 0;
rechargeAmount.value = 500;
customAmount.value = 0;
payType.value = 'zfb';
fileList.value = [];
screenshotFile.value = null;
if (previewUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl.value);
}
previewUrl.value = '';
};
const goToRecord = () => {
router.push({ name: 'FinanceRechargeRecord' });
};
</script>
<style lang="less" scoped>
.deposit-page {
min-height: 100vh;
padding: 40px 0 60px;
background: radial-gradient(circle at top, #eff4ff, #f7f8fb 60%);
display: flex;
align-items: center;
}
.deposit-layout {
width: 1000px;
margin: 0 auto;
display: grid;
grid-template-columns: 320px 1fr;
gap: 24px;
}
.info-panel {
padding: 24px;
background: #0d1d3d;
border-radius: 24px;
color: #f5f8ff;
display: flex;
flex-direction: column;
gap: 18px;
box-shadow: 0 20px 40px rgba(1, 14, 38, 0.25);
}
.panel-subtitle {
font-size: 13px;
letter-spacing: 2px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.6);
}
.panel-header h2 {
font-size: 24px;
margin: 4px 0;
}
.panel-desc {
color: rgba(255, 255, 255, 0.75);
}
.steps {
padding: 0;
:deep(.arco-steps-item-title) {
color: #fff;
font-weight: 600;
}
:deep(.arco-steps-item-description) {
color: rgba(255, 255, 255, 0.7);
}
:deep(.arco-steps-icon) {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
}
.tips-card {
margin-top: auto;
padding: 14px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.08);
ul {
margin: 6px 0 0;
padding-left: 18px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.8);
}
}
.tips-title {
font-weight: 600;
}
.form-panel {
padding: 28px 32px 34px;
background: var(--app-card-bg);
border-radius: 32px;
box-shadow: 0 20px 60px rgba(31, 52, 108, 0.12);
display: flex;
flex-direction: column;
gap: 22px;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.amount-box {
display: flex;
flex-direction: column;
}
.amount-label {
font-size: 13px;
color: #8a93a8;
}
.amount-value {
font-size: 30px;
font-weight: 700;
color: #1f2f4d;
}
.step-chip {
padding: 6px 14px;
border-radius: 999px;
background: #eef3ff;
color: #2b63ff;
font-weight: 600;
}
.form-block + .form-block {
margin-top: 8px;
}
.block-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 10px;
color: #1f2f4d;
}
.block-desc {
margin-top: -4px;
margin-bottom: 12px;
color: #6f7c97;
}
.amount-buttons {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
:deep(.arco-radio-button) {
text-align: center;
border-radius: 16px;
font-weight: 600;
border: 1px solid #d7dff0;
transition: all 0.2s ease;
line-height: 34px;
height: 44px;
&.arco-radio-checked {
border-color: #2b63ff;
color: #2b63ff;
box-shadow: 0 8px 18px rgba(43, 99, 255, 0.22);
background: rgba(43, 99, 255, 0.08);
}
}
}
.custom-amount {
margin-top: 14px;
display: flex;
align-items: center;
gap: 12px;
label {
color: #6f7c97;
font-weight: 600;
}
}
.pay-type-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-bottom: 18px;
}
.pay-card {
border: 1px solid #e4eaf5;
border-radius: 20px;
padding: 14px 18px;
display: flex;
gap: 14px;
align-items: center;
background: #f8faff;
cursor: pointer;
transition: all 0.2s ease;
img {
width: 40px;
height: 40px;
}
&.active {
border-color: #2b63ff;
background: #ecf1ff;
box-shadow: 0 14px 28px rgba(43, 99, 255, 0.18);
}
}
.pay-name {
font-weight: 600;
color: #1f2f4d;
}
.pay-desc {
font-size: 12px;
color: #7f8ba8;
}
.qr-wrapper {
text-align: center;
}
.qr-image {
border: 2px dashed #dbe3f4;
border-radius: 16px;
padding: 14px;
background: #fdfefe;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
:deep(.arco-image),
:deep(.arco-image-with-footer-inner) {
width: 220px !important;
height: 220px !important;
}
:deep(.arco-image-img) {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 12px;
}
}
.qr-tip {
margin-top: 8px;
font-size: 13px;
color: #7b869f;
}
.upload-card {
width: 200px;
height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px dashed #d7dff0;
border-radius: 24px;
color: #9da9bf;
background: #f8faff;
gap: 8px;
.arco-icon {
font-size: 30px;
}
}
.preview-wrap {
margin-top: 12px;
}
.preview-image {
max-width: 220px;
max-height: 220px;
border-radius: 12px;
border: 1px solid var(--color-border-2);
object-fit: contain;
}
.action-row {
margin-top: 10px;
display: flex;
gap: 14px;
justify-content: flex-end;
}
:deep(.arco-upload-list-picture-card) {
.arco-upload-list-item {
width: 220px;
height: 220px;
border-radius: 24px;
}
}
@media (max-width: 1200px) {
.deposit-layout {
width: 94%;
grid-template-columns: 1fr;
}
.info-panel {
order: 2;
}
}
</style>