feat: migrate app routes and business modules
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user