feat: migrate app routes and business modules

This commit is contained in:
2026-08-12 18:22:59 +08:00
parent 555aa00043
commit cc706378c2
247 changed files with 28623 additions and 14988 deletions
@@ -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>