305 lines
8.6 KiB
Vue
305 lines
8.6 KiB
Vue
<template>
|
||
<AppDialog
|
||
:visible="formVisible"
|
||
eyebrow="提现申请"
|
||
title="填写收款信息"
|
||
message="提交后将冻结对应收益,审核通过后按收款码转账。"
|
||
confirm-text="核对并提交"
|
||
cancel-text="暂不提现"
|
||
show-cancel
|
||
:close-on-mask="!submitting && !uploading"
|
||
@confirm="reviewWithdrawal"
|
||
@cancel="close"
|
||
>
|
||
<view class="withdrawal-form">
|
||
<text>提现金额(元)</text>
|
||
<input
|
||
v-model.trim="withdrawalForm.amount"
|
||
type="digit"
|
||
maxlength="19"
|
||
placeholder="请输入提现金额"
|
||
/>
|
||
<text>收款人姓名</text>
|
||
<input
|
||
v-model.trim="withdrawalForm.payoutAccountName"
|
||
maxlength="64"
|
||
placeholder="请输入收款码对应姓名"
|
||
/>
|
||
<view
|
||
class="qr-picker"
|
||
role="button"
|
||
aria-label="选择收款码图片"
|
||
@click="choosePayoutQr"
|
||
>
|
||
<image
|
||
v-if="withdrawalForm.qrPreview"
|
||
:src="withdrawalForm.qrPreview"
|
||
mode="aspectFit"
|
||
/>
|
||
<text>{{ payoutQrPrompt }}</text>
|
||
</view>
|
||
<text v-if="withdrawalError" class="form-error">{{ withdrawalError }}</text>
|
||
</view>
|
||
</AppDialog>
|
||
|
||
<AppDialog
|
||
:visible="confirmationVisible"
|
||
title="确认申请提现吗?"
|
||
:message="confirmationMessage"
|
||
:confirm-text="submitting ? '正在提交' : '确认提交'"
|
||
cancel-text="返回修改"
|
||
show-cancel
|
||
:close-on-mask="false"
|
||
@confirm="submitWithdrawal"
|
||
@cancel="confirmationVisible = false"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onUnmounted, reactive, ref, watch } from "vue";
|
||
import AppDialog from "@/components/AppDialog.vue";
|
||
import {
|
||
createRequestController,
|
||
isRequestCancelled
|
||
} from "@/services/api/request-controller.js";
|
||
import { earningApi } from "@/services/api/earning-service.js";
|
||
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
|
||
import {
|
||
isImagePickCancelled,
|
||
pickAndUploadImage,
|
||
} from "@/utils/media-upload.js";
|
||
import { parseMoneyToCents } from "@/utils/profile/earning-money.js";
|
||
import { isWriteOutcomeUnknown } from "@/utils/request-outcome.js";
|
||
|
||
const props = defineProps({
|
||
availableAmount: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
minimumWithdrawal: {
|
||
type: String,
|
||
default: null,
|
||
},
|
||
afterSubmitted: {
|
||
type: Function,
|
||
required: true,
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits(["busy-change", "transient-change"]);
|
||
|
||
const formVisible = ref(false);
|
||
const confirmationVisible = ref(false);
|
||
const withdrawalError = ref("");
|
||
const uploading = ref(false);
|
||
const submitting = ref(false);
|
||
const withdrawalForm = reactive({
|
||
requestId: "",
|
||
amount: "",
|
||
payoutAccountName: "",
|
||
payoutQrOssId: null,
|
||
qrPreview: "",
|
||
});
|
||
const payoutQrUploadRequestController = createRequestController();
|
||
const withdrawalSubmissionRequestController = createRequestController();
|
||
let componentActive = true;
|
||
|
||
const isBusy = computed(() => uploading.value || submitting.value);
|
||
const hasTransient = computed(() =>
|
||
formVisible.value || confirmationVisible.value,
|
||
);
|
||
const payoutQrPrompt = computed(() => {
|
||
if (uploading.value) return "正在上传收款码…";
|
||
return withdrawalForm.payoutQrOssId
|
||
? "重新选择收款码"
|
||
: "选择微信或支付宝收款码";
|
||
});
|
||
const confirmationMessage = computed(() =>
|
||
`本次申请 ¥${withdrawalForm.amount || "0.00"},收款人 ${
|
||
withdrawalForm.payoutAccountName || "未填写"
|
||
}。请确认金额和收款码无误。`,
|
||
);
|
||
|
||
watch(isBusy, (busy) => emit("busy-change", busy), { immediate: true });
|
||
watch(hasTransient, (visible) => emit("transient-change", visible), {
|
||
immediate: true,
|
||
});
|
||
|
||
const createWithdrawalRequestId = () =>
|
||
typeof globalThis.crypto?.randomUUID === "function"
|
||
? globalThis.crypto.randomUUID()
|
||
: `withdrawal-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||
const validateWithdrawal = () => {
|
||
const amountInCents = parseMoneyToCents(withdrawalForm.amount);
|
||
const availableInCents = parseMoneyToCents(props.availableAmount);
|
||
const minimumInCents = parseMoneyToCents(props.minimumWithdrawal || "0.01");
|
||
if (amountInCents === null || amountInCents <= 0n) {
|
||
return "请输入正确的提现金额,最多保留两位小数。";
|
||
}
|
||
if (minimumInCents !== null && amountInCents < minimumInCents) {
|
||
return `提现金额不能低于 ¥${props.minimumWithdrawal || "0.01"}。`;
|
||
}
|
||
if (availableInCents !== null && amountInCents > availableInCents) {
|
||
return "提现金额不能超过当前可用收益。";
|
||
}
|
||
if (!withdrawalForm.payoutAccountName.trim()) return "请填写收款人姓名。";
|
||
if (!withdrawalForm.payoutQrOssId) return "请选择收款码图片。";
|
||
return "";
|
||
};
|
||
|
||
const open = () => {
|
||
if (isBusy.value) return false;
|
||
Object.assign(withdrawalForm, {
|
||
requestId: createWithdrawalRequestId(),
|
||
amount: props.minimumWithdrawal || "",
|
||
payoutAccountName: "",
|
||
payoutQrOssId: null,
|
||
qrPreview: "",
|
||
});
|
||
withdrawalError.value = "";
|
||
formVisible.value = true;
|
||
return true;
|
||
};
|
||
const close = () => {
|
||
if (isBusy.value) return false;
|
||
formVisible.value = false;
|
||
confirmationVisible.value = false;
|
||
return true;
|
||
};
|
||
const closeTransient = () => {
|
||
if (confirmationVisible.value && !isBusy.value) {
|
||
confirmationVisible.value = false;
|
||
return true;
|
||
}
|
||
return formVisible.value ? close() : false;
|
||
};
|
||
const choosePayoutQr = async () => {
|
||
if (isBusy.value) return;
|
||
uploading.value = true;
|
||
withdrawalError.value = "";
|
||
try {
|
||
const payoutQrUpload = await pickAndUploadImage({
|
||
requestController: payoutQrUploadRequestController,
|
||
});
|
||
if (!componentActive) return;
|
||
withdrawalForm.payoutQrOssId = payoutQrUpload.ossId;
|
||
withdrawalForm.qrPreview =
|
||
payoutQrUpload.thumbnailUrl || payoutQrUpload.url;
|
||
} catch (error) {
|
||
if (
|
||
componentActive &&
|
||
!isImagePickCancelled(error) &&
|
||
!isRequestCancelled(error)
|
||
) {
|
||
withdrawalError.value = getRequestErrorMessage(
|
||
error,
|
||
"收款码上传失败,请重新选择。",
|
||
);
|
||
}
|
||
} finally {
|
||
if (componentActive) uploading.value = false;
|
||
}
|
||
};
|
||
const reviewWithdrawal = () => {
|
||
if (isBusy.value) return;
|
||
withdrawalError.value = validateWithdrawal();
|
||
if (!withdrawalError.value) confirmationVisible.value = true;
|
||
};
|
||
const submitWithdrawal = async () => {
|
||
if (submitting.value) return;
|
||
withdrawalError.value = validateWithdrawal();
|
||
if (withdrawalError.value) {
|
||
confirmationVisible.value = false;
|
||
return;
|
||
}
|
||
submitting.value = true;
|
||
let withdrawalCommitted = false;
|
||
try {
|
||
await earningApi.requestEarningWithdrawal(
|
||
{
|
||
requestId: withdrawalForm.requestId,
|
||
amount: withdrawalForm.amount,
|
||
payoutQrOssId: withdrawalForm.payoutQrOssId,
|
||
payoutAccountName: withdrawalForm.payoutAccountName,
|
||
},
|
||
{ requestController: withdrawalSubmissionRequestController },
|
||
);
|
||
withdrawalCommitted = true;
|
||
if (!componentActive) return;
|
||
confirmationVisible.value = false;
|
||
formVisible.value = false;
|
||
await props.afterSubmitted();
|
||
} catch (error) {
|
||
if (!componentActive) return;
|
||
confirmationVisible.value = false;
|
||
if (withdrawalCommitted) {
|
||
formVisible.value = false;
|
||
return;
|
||
}
|
||
if (isWriteOutcomeUnknown(error)) {
|
||
withdrawalError.value =
|
||
"暂时无法确认是否提交成功,请先关闭窗口查看提现记录,不要重复提交。";
|
||
return;
|
||
}
|
||
if (!isRequestCancelled(error))
|
||
withdrawalError.value = getRequestErrorMessage(
|
||
error,
|
||
"提现申请未提交,请核对后重试。",
|
||
);
|
||
} finally {
|
||
if (componentActive) submitting.value = false;
|
||
}
|
||
};
|
||
|
||
defineExpose({ closeTransient, open });
|
||
|
||
onUnmounted(() => {
|
||
componentActive = false;
|
||
payoutQrUploadRequestController.abort();
|
||
withdrawalSubmissionRequestController.abort();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.withdrawal-form {
|
||
width: 100%;
|
||
margin-top: 14rpx;
|
||
text-align: left;
|
||
}
|
||
.withdrawal-form > text {
|
||
display: block;
|
||
margin: 14rpx 0 7rpx;
|
||
color: $ink;
|
||
font-size: clamp(14px, 22rpx, 17px);
|
||
}
|
||
.withdrawal-form input {
|
||
box-sizing: border-box;
|
||
min-height: 76rpx;
|
||
padding: 0 18rpx;
|
||
border: 1rpx solid rgba(159, 35, 35, 0.25);
|
||
background: #fffdf7;
|
||
color: $ink;
|
||
}
|
||
.qr-picker {
|
||
display: flex;
|
||
min-height: 90rpx;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 18rpx;
|
||
padding: 12rpx;
|
||
border: 1rpx dashed rgba(159, 35, 35, 0.45);
|
||
color: $brand-red;
|
||
text-align: center;
|
||
gap: 16rpx;
|
||
}
|
||
.qr-picker image {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
}
|
||
.form-error {
|
||
color: $brand-red !important;
|
||
line-height: 1.5;
|
||
text-align: center;
|
||
}
|
||
</style>
|