新增代发订单列表、扫码反查与拣货页,完善绑定/进度展示与 nginx/vite SPA 回退;同步 SKU 申报、价格发布跳过、字典与 H5 代发体验优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
462 lines
9.9 KiB
Vue
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>
|