feat: 添加支付方式管理功能,创建支付方式列表和添加支付方式页面,集成表单验证和状态管理

This commit is contained in:
2026-01-18 03:17:03 +07:00
parent 46c198c101
commit f5255bfb9d
3 changed files with 835 additions and 0 deletions

474
src/views/payment/add.vue Normal file
View File

@@ -0,0 +1,474 @@
<script lang='ts' setup>
import { toastController } from "@ionic/vue";
import { cardOutline, checkmarkCircleOutline, logoAlipay, personOutline } from "ionicons/icons";
import zod from "zod";
const router = useRouter();
const route = useRoute();
const paymentType = ref<"bank" | "alipay">("bank");
const bankFormData = ref({
name: "",
bankName: "",
bankBranch: "",
cardNumber: "",
isDefault: false,
});
const alipayFormData = ref({
name: "",
account: "",
isDefault: false,
});
const isSubmitting = ref(false);
const isEditMode = computed(() => !!route.query.id);
// 银行卡表单验证 Schema
const BankSchema = zod.object({
name: zod
.string()
.min(2, "请输入持卡人姓名")
.max(20, "姓名长度不能超过20个字符"),
bankName: zod
.string()
.min(2, "请输入银行名称")
.max(50, "银行名称长度不能超过50个字符"),
bankBranch: zod
.string()
.min(2, "请输入开户行")
.max(100, "开户行长度不能超过100个字符"),
cardNumber: zod
.string()
.min(1, "请输入银行卡号")
.regex(/^\d{16,19}$/, "请输入正确的银行卡号16-19位数字"),
});
// 支付宝表单验证 Schema
const AlipaySchema = zod.object({
name: zod
.string()
.min(2, "请输入真实姓名")
.max(20, "姓名长度不能超过20个字符"),
account: zod
.string()
.min(1, "请输入支付宝账号")
.refine(
(val) => {
// 支持手机号或邮箱
const phoneRegex = /^1[3-9]\d{9}$/;
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
return phoneRegex.test(val) || emailRegex.test(val);
},
"请输入正确的支付宝账号(手机号或邮箱)",
),
});
async function showToast(message: string, color: "success" | "danger" | "warning" = "danger") {
const toast = await toastController.create({
message,
duration: 2000,
position: "top",
color,
});
await toast.present();
}
// 如果是编辑模式,加载数据
onMounted(() => {
if (isEditMode.value) {
// TODO: 根据 route.query.id 加载收款方式数据
// 模拟数据
paymentType.value = "bank";
bankFormData.value = {
name: "张三",
bankName: "中国工商银行",
bankBranch: "北京朝阳支行",
cardNumber: "6222021234567891234",
isDefault: true,
};
}
});
async function handleSubmit() {
let result;
if (paymentType.value === "bank") {
result = BankSchema.safeParse(bankFormData.value);
}
else {
result = AlipaySchema.safeParse(alipayFormData.value);
}
if (!result.success) {
const first = result.error.issues[0];
await showToast(first.message);
return;
}
isSubmitting.value = true;
try {
// TODO: 调用添加/编辑收款方式 API
// const formData = paymentType.value === "bank" ? bankFormData.value : alipayFormData.value;
// if (isEditMode.value) {
// await safeClient(client.api.payment[route.query.id].put({ ...formData, type: paymentType.value }));
// } else {
// await safeClient(client.api.payment.post({ ...formData, type: paymentType.value }));
// }
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000));
await showToast(isEditMode.value ? "收款方式修改成功" : "收款方式添加成功", "success");
router.back();
}
catch (error) {
await showToast("操作失败,请重试", "danger");
}
finally {
isSubmitting.value = false;
}
}
</script>
<template>
<ion-page>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ion-buttons slot="start">
<back-button />
</ion-buttons>
<ion-title>{{ isEditMode ? '编辑收款方式' : '添加收款方式' }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="space-y-5">
<!-- 提示信息 -->
<div class="info-card">
<ion-icon :icon="checkmarkCircleOutline" class="text-2xl text-[#c41e3a]" />
<div class="text-sm text-[#666] leading-relaxed">
请填写准确的收款信息确保能够正常收款
</div>
</div>
<!-- 类型选择 -->
<div v-if="!isEditMode" class="type-selector">
<div class="type-title">
选择收款方式类型
</div>
<div class="type-buttons">
<div
class="type-button"
:class="{ active: paymentType === 'bank' }"
@click="paymentType = 'bank'"
>
<ion-icon :icon="cardOutline" class="type-icon" />
<span>银行卡</span>
</div>
<div
class="type-button"
:class="{ active: paymentType === 'alipay' }"
@click="paymentType = 'alipay'"
>
<ion-icon :icon="logoAlipay" class="type-icon" />
<span>支付宝</span>
</div>
</div>
</div>
<!-- 银行卡表单 -->
<div v-if="paymentType === 'bank'" class="form-card">
<div class="space-y-4">
<!-- 持卡人姓名 -->
<div class="form-item">
<div class="flex items-center gap-2 mb-2">
<ion-icon :icon="personOutline" class="text-lg text-primary" />
<label class="form-label">持卡人姓名</label>
</div>
<ion-item lines="none" class="input-item">
<ion-input
v-model="bankFormData.name"
type="text"
placeholder="请输入持卡人姓名"
class="custom-input"
:maxlength="20"
/>
</ion-item>
</div>
<!-- 银行名称 -->
<div class="form-item">
<div class="flex items-center gap-2 mb-2">
<ion-icon :icon="cardOutline" class="text-lg text-primary" />
<label class="form-label">银行名称</label>
</div>
<ion-item lines="none" class="input-item">
<ion-input
v-model="bankFormData.bankName"
type="text"
placeholder="例如:中国工商银行"
class="custom-input"
:maxlength="50"
/>
</ion-item>
</div>
<!-- 开户行 -->
<div class="form-item">
<div class="flex items-center gap-2 mb-2">
<ion-icon :icon="cardOutline" class="text-lg text-primary" />
<label class="form-label">开户行</label>
</div>
<ion-item lines="none" class="input-item">
<ion-input
v-model="bankFormData.bankBranch"
type="text"
placeholder="例如:北京朝阳支行"
class="custom-input"
:maxlength="100"
/>
</ion-item>
</div>
<!-- 银行卡号 -->
<div class="form-item">
<div class="flex items-center gap-2 mb-2">
<ion-icon :icon="cardOutline" class="text-lg text-primary" />
<label class="form-label">银行卡号</label>
</div>
<ion-item lines="none" class="input-item">
<ion-input
v-model="bankFormData.cardNumber"
type="tel"
placeholder="请输入银行卡号"
class="custom-input"
:maxlength="19"
/>
</ion-item>
</div>
<!-- 设为默认 -->
<div class="form-item">
<div class="flex items-center justify-between">
<label class="form-label">设为默认收款方式</label>
<ion-toggle v-model="bankFormData.isDefault" color="danger" />
</div>
</div>
</div>
</div>
<!-- 支付宝表单 -->
<div v-else class="form-card">
<div class="space-y-4">
<!-- 支付宝姓名 -->
<div class="form-item">
<div class="flex items-center gap-2 mb-2">
<ion-icon :icon="personOutline" class="text-lg text-[#1677ff]" />
<label class="form-label">支付宝姓名</label>
</div>
<ion-item lines="none" class="input-item">
<ion-input
v-model="alipayFormData.name"
type="text"
placeholder="请输入支付宝实名姓名"
class="custom-input"
:maxlength="20"
/>
</ion-item>
</div>
<!-- 支付宝账号 -->
<div class="form-item">
<div class="flex items-center gap-2 mb-2">
<ion-icon :icon="logoAlipay" class="text-lg text-[#1677ff]" />
<label class="form-label">支付宝账号</label>
</div>
<ion-item lines="none" class="input-item">
<ion-input
v-model="alipayFormData.account"
type="text"
placeholder="请输入手机号或邮箱"
class="custom-input"
/>
</ion-item>
<div class="text-xs text-[#999] mt-1 ml-1">
支持手机号或邮箱格式
</div>
</div>
<!-- 设为默认 -->
<div class="form-item">
<div class="flex items-center justify-between">
<label class="form-label">设为默认收款方式</label>
<ion-toggle v-model="alipayFormData.isDefault" color="danger" />
</div>
</div>
</div>
</div>
<!-- 提交按钮 -->
<ion-button
expand="block"
class="submit-button"
:disabled="isSubmitting"
@click="handleSubmit"
>
<ion-spinner v-if="isSubmitting" name="crescent" />
<span v-else>{{ isEditMode ? '保存修改' : '保存收款方式' }}</span>
</ion-button>
<!-- 安全提示 -->
<div class="notice-card">
<div class="text-sm font-semibold text-[#333] mb-2">
安全提示
</div>
<ul class="text-xs text-[#666] space-y-1 pl-4">
<li> 请确保填写的收款信息准确无误</li>
<li> 收款信息仅用于提现和收款我们会严格保密</li>
<li> 银行卡需与实名认证信息一致</li>
<li> 如需修改收款方式请联系客服</li>
</ul>
</div>
</div>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped>
.info-card {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, #fff5f5 0%, #ffffff 100%);
border-radius: 12px;
border: 1px solid #ffe0e0;
}
.type-selector {
background: white;
border-radius: 16px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.type-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.type-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.type-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px 16px;
background: #f7f8fa;
border: 2px solid #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.type-button.active {
background: linear-gradient(135deg, #fff5f5 0%, #ffffff 100%);
border-color: #c41e3a;
}
.type-button:active {
transform: scale(0.98);
}
.type-icon {
font-size: 32px;
color: #666;
margin-bottom: 8px;
}
.type-button.active .type-icon {
color: #c41e3a;
}
.type-button span {
font-size: 14px;
font-weight: 600;
color: #666;
}
.type-button.active span {
color: #c41e3a;
}
.form-card {
background: white;
border-radius: 16px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
.form-item {
margin-bottom: 20px;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 15px;
font-weight: 600;
color: #333;
}
.input-item {
--background: #f7f8fa;
--border-radius: 12px;
--padding-start: 16px;
--padding-end: 16px;
--min-height: 48px;
}
.custom-input {
--placeholder-color: #999;
--placeholder-opacity: 1;
font-size: 15px;
}
.submit-button {
--background: linear-gradient(135deg, #c41e3a 0%, #8b1a2e 100%);
--background-activated: linear-gradient(135deg, #8b1a2e 0%, #c41e3a 100%);
--border-radius: 12px;
--padding-top: 14px;
--padding-bottom: 14px;
font-weight: 600;
font-size: 16px;
margin-top: 8px;
text-transform: none;
letter-spacing: 0.5px;
}
.notice-card {
background: #f9fafb;
border-radius: 12px;
padding: 16px;
border: 1px solid #e5e7eb;
}
.notice-card ul li {
line-height: 1.8;
}
</style>