front/src/views/h5/customer-center/components/H5CommissionSheet.vue
zzh 8155ba93d9 feat: 代发订单分轨与运营页,补 SPA 回退与 H5/下单优化
新增代发订单列表、扫码反查与拣货页,完善绑定/进度展示与 nginx/vite SPA 回退;同步 SKU 申报、价格发布跳过、字典与 H5 代发体验优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 10:27:37 +08:00

462 lines
9.9 KiB
Vue

<template>
<a-drawer
:visible="visible"
placement="bottom"
height="auto"
:footer="false"
:header="false"
unmount-on-close
:mask-closable="!transferring && !withdrawing"
:drawer-style="drawerStyle"
popup-container="body"
class="h5-commission-drawer"
@cancel="close"
>
<div class="h5-cm">
<div class="h5-cm__handle" aria-hidden="true" />
<header class="h5-cm__head">
<h3>{{ t('h5.commission.title') }}</h3>
<button
type="button"
class="h5-cm__close"
:disabled="transferring || withdrawing"
@click="close"
>
<icon-close />
</button>
</header>
<a-spin :loading="loading" class="h5-cm__body">
<H5EmptyState
v-if="!overview && !loading"
:icon="IconTrophy"
:text="t('h5.commission.empty')"
/>
<template v-else-if="overview">
<section class="hero-metric">
<span class="label">{{ t('h5.commission.balance') }}</span>
<strong>¥{{ formatMoney(overview.availableBalance) }}</strong>
<div class="hero-metric__sub">
<span>
{{ t('h5.commission.total') }}
<b>¥{{ formatMoney(overview.totalEarned) }}</b>
</span>
<span>
{{ t('h5.commission.pending') }}
<b>¥{{ formatMoney(overview.pendingAmount) }}</b>
</span>
</div>
</section>
<section class="action-card">
<p class="field-label">{{ t('h5.commission.amountPh') }}</p>
<div class="amount-focus">
<span class="amount-focus__prefix">¥</span>
<a-input-number
v-model="amount"
:min="0.01"
:precision="2"
:hide-button="true"
class="amount-focus__input"
:placeholder="'0.00'"
/>
</div>
<div class="actions">
<button
type="button"
class="act-btn is-ghost"
:disabled="transferring || withdrawing"
@click="handleTransfer"
>
<span v-if="transferring" class="act-btn__loading" aria-hidden="true" />
{{ t('h5.commission.transfer') }}
</button>
<button
type="button"
class="act-btn is-primary"
:disabled="transferring || withdrawing"
@click="handleWithdraw"
>
<span v-if="withdrawing" class="act-btn__loading" aria-hidden="true" />
{{ t('h5.commission.withdraw') }}
</button>
</div>
</section>
</template>
</a-spin>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { Message } from '@arco-design/web-vue';
import { IconClose, IconTrophy } from '@arco-design/web-vue/es/icon';
import {
applyCommissionWithdraw,
getCommissionOverview,
transferCommissionToBalance,
} from '@/api/marketing/portal';
import H5EmptyState from './H5EmptyState.vue';
defineOptions({ name: 'H5CommissionSheet' });
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
const { t } = useI18n();
const loading = ref(false);
const transferring = ref(false);
const withdrawing = ref(false);
const amount = ref<number | undefined>();
const overview = ref<{
availableBalance: number;
totalEarned: number;
pendingAmount: number;
} | null>(null);
const drawerStyle = {
borderRadius: '20px 20px 0 0',
overflow: 'hidden',
maxHeight: 'min(92vh, 780px)',
};
function formatMoney(v: unknown) {
const n = Number(v);
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
}
function close() {
if (transferring.value || withdrawing.value) return;
emit('update:visible', false);
}
async function load() {
loading.value = true;
try {
const raw: any = await getCommissionOverview();
overview.value = {
availableBalance: Number(
raw?.availableBalance ?? raw?.AvailableBalance ?? raw?.commissionBalance ?? 0,
),
totalEarned: Number(raw?.totalEarned ?? raw?.TotalEarned ?? 0),
pendingAmount: Number(raw?.pendingAmount ?? raw?.PendingAmount ?? 0),
};
} catch {
overview.value = null;
} finally {
loading.value = false;
}
}
async function handleTransfer() {
if (!amount.value || amount.value <= 0) {
Message.warning(t('h5.commission.amountPh'));
return;
}
transferring.value = true;
try {
await transferCommissionToBalance({ amount: amount.value });
Message.success(t('h5.commission.transferOk'));
amount.value = undefined;
await load();
} catch (e: any) {
Message.error(e?.message || t('h5.commission.empty'));
} finally {
transferring.value = false;
}
}
async function handleWithdraw() {
if (!amount.value || amount.value <= 0) {
Message.warning(t('h5.commission.amountPh'));
return;
}
withdrawing.value = true;
try {
await applyCommissionWithdraw({ amount: amount.value });
Message.success(t('h5.commission.withdrawOk'));
amount.value = undefined;
await load();
} catch (e: any) {
Message.error(e?.message || t('h5.commission.empty'));
} finally {
withdrawing.value = false;
}
}
watch(
() => props.visible,
(visible) => {
if (visible) {
amount.value = undefined;
void load();
}
},
{ immediate: true },
);
</script>
<style lang="less">
.h5-commission-drawer {
.arco-drawer-content {
border-radius: 20px 20px 0 0;
overflow: hidden;
max-height: min(92vh, 780px);
}
.arco-drawer-body {
padding: 0;
display: flex;
flex-direction: column;
max-height: min(92vh, 780px);
}
}
</style>
<style lang="less" scoped>
.h5-cm {
display: flex;
flex-direction: column;
max-height: min(92vh, 780px);
padding-bottom: env(safe-area-inset-bottom, 0px);
background: #f5f7f8;
}
.h5-cm__handle {
width: 36px;
height: 4px;
margin: 10px auto 0;
border-radius: 999px;
background: #c9cdd4;
flex-shrink: 0;
}
.h5-cm__head {
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: 48px;
flex-shrink: 0;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1d2129;
}
}
.h5-cm__close {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
border: none;
border-radius: 10px;
background: transparent;
color: #86909c;
font-size: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:disabled {
opacity: 0.5;
}
}
.h5-cm__body {
flex: 1;
min-height: 220px;
overflow: auto;
display: block;
width: 100%;
padding: 0 16px 16px;
box-sizing: border-box;
}
.hero-metric {
margin-bottom: 12px;
padding: 20px 18px 16px;
border-radius: 16px;
background: linear-gradient(
145deg,
color-mix(in srgb, var(--app-primary-color) 88%, #0f766e),
var(--app-primary-color)
);
color: #fff;
box-shadow: 0 10px 24px color-mix(in srgb, var(--app-primary-color) 24%, transparent);
.label {
display: block;
font-size: 13px;
opacity: 0.9;
}
strong {
display: block;
margin-top: 8px;
font-size: 34px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
line-height: 1.1;
}
}
.hero-metric__sub {
display: flex;
gap: 12px;
margin-top: 16px;
span {
flex: 1;
padding: 10px 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.14);
font-size: 12px;
opacity: 0.95;
line-height: 1.4;
}
b {
display: block;
margin-top: 4px;
font-size: 15px;
font-weight: 600;
opacity: 1;
font-variant-numeric: tabular-nums;
}
}
.action-card {
background: #fff;
border-radius: 16px;
padding: 18px 16px;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
}
.field-label {
margin: 0 0 10px;
font-size: 13px;
color: #4e5969;
}
.amount-focus {
display: flex;
align-items: center;
gap: 4px;
min-height: 64px;
padding: 8px 16px;
border-radius: 14px;
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
}
.amount-focus__prefix {
font-size: 24px;
font-weight: 700;
color: var(--app-primary-color);
line-height: 1;
}
.amount-focus__input {
flex: 1;
width: 100%;
:deep(.arco-input-wrapper) {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0;
}
:deep(.arco-input) {
font-size: 28px;
font-weight: 700;
color: #1d2129;
font-variant-numeric: tabular-nums;
height: 40px;
line-height: 40px;
}
:deep(.arco-input::placeholder) {
color: #c9cdd4;
font-weight: 600;
}
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 16px;
}
.act-btn {
height: 48px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
&.is-ghost {
border: none;
background: #f2f3f5;
color: #4e5969;
&:active:not(:disabled) {
background: #e5e6eb;
}
}
&.is-primary {
border: none;
background: var(--app-primary-color);
color: #fff;
box-shadow: 0 8px 18px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
&:active:not(:disabled) {
opacity: 0.92;
}
}
&:disabled {
opacity: 0.55;
cursor: not-allowed;
box-shadow: none;
}
}
.act-btn__loading {
width: 16px;
height: 16px;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
opacity: 0.7;
animation: h5-cm-spin 0.7s linear infinite;
}
@keyframes h5-cm-spin {
to {
transform: rotate(360deg);
}
}
</style>