feat: 添加产品申购功能,集成钱包选择和交易密码验证

This commit is contained in:
2026-01-20 05:27:54 +07:00
parent 2fc878be07
commit 532b229299
5 changed files with 359 additions and 2 deletions

3
auto-imports.d.ts vendored
View File

@@ -334,6 +334,9 @@ declare global {
export type { UploadFetchOptions } from './src/utils/aws/s3'
import('./src/utils/aws/s3')
// @ts-ignore
export type { PageInstance, InputInstance, ModalInstance, ContentInstance } from './src/utils/ionic-helper'
import('./src/utils/ionic-helper')
// @ts-ignore
export type { Wallet } from './src/store/wallet'
import('./src/store/wallet')
}

6
components.d.ts vendored
View File

@@ -30,7 +30,10 @@ declare module 'vue' {
IonItem: typeof import('@ionic/vue')['IonItem']
IonLabel: typeof import('@ionic/vue')['IonLabel']
IonList: typeof import('@ionic/vue')['IonList']
IonModal: typeof import('@ionic/vue')['IonModal']
IonPage: typeof import('@ionic/vue')['IonPage']
IonRadio: typeof import('@ionic/vue')['IonRadio']
IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
IonRefresher: typeof import('@ionic/vue')['IonRefresher']
IonRefresherContent: typeof import('@ionic/vue')['IonRefresherContent']
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
@@ -72,7 +75,10 @@ declare global {
const IonItem: typeof import('@ionic/vue')['IonItem']
const IonLabel: typeof import('@ionic/vue')['IonLabel']
const IonList: typeof import('@ionic/vue')['IonList']
const IonModal: typeof import('@ionic/vue')['IonModal']
const IonPage: typeof import('@ionic/vue')['IonPage']
const IonRadio: typeof import('@ionic/vue')['IonRadio']
const IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
const IonRefresher: typeof import('@ionic/vue')['IonRefresher']
const IonRefresherContent: typeof import('@ionic/vue')['IonRefresherContent']
const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']

View File

@@ -0,0 +1,4 @@
export type PageInstance = InstanceType<typeof import("@ionic/vue").IonPage>;
export type InputInstance = InstanceType<typeof import("@ionic/vue").IonInput>;
export type ModalInstance = InstanceType<typeof import("@ionic/vue").IonModal>;
export type ContentInstance = InstanceType<typeof import("@ionic/vue").IonContent>;

View File

@@ -0,0 +1,322 @@
<script lang='ts' setup>
import type { Wallet } from "@/store/wallet";
import { toastController } from "@ionic/vue";
import { checkmarkCircleOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
const props = defineProps<{
productId: number;
wallets: Array<Wallet>;
}>();
const emit = defineEmits<{
close: [];
success: [];
}>();
const { data: product } = safeClient(() => client.api.subscription.products({ productId: props.productId }).get());
const paymentPassword = ref("");
const selectedWallet = ref<string | null>(null);
// 当前选中的钱包信息
const currentWallet = computed(() => {
if (!selectedWallet.value) {
return props.wallets[0];
}
return props.wallets.find(w => w.walletTypeId === selectedWallet.value);
});
watchEffect(() => {
if (props.wallets.length > 0 && !selectedWallet.value) {
selectedWallet.value = props.wallets[0].walletTypeId;
}
});
function handleClose() {
emit("close");
}
async function confirmSubscribe() {
if (!product.value) {
return;
}
if (!paymentPassword.value) {
toastController.create({
message: "请输入交易密码",
duration: 2000,
position: "top",
}).then(toast => toast.present());
return;
}
if (!currentWallet.value || Number(currentWallet.value?.available) < Number(product.value.price)) {
toastController.create({
message: "所选钱包余额不足,请选择其他钱包或充值",
duration: 2000,
position: "top",
}).then(toast => toast.present());
return;
}
await safeClient(client.api.subscription.orders.post({
productId: product.value.id,
walletTypeId: selectedWallet.value,
tradePassword: paymentPassword.value,
}));
const toast = await toastController.create({
message: "产品申购成功!",
duration: 2000,
position: "bottom",
color: "success",
});
await toast.present();
setTimeout(() => {
toast.dismiss();
emit("success");
}, 2000);
}
</script>
<template>
<ion-page>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ion-title>产品申购</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding" :fullscreen="true">
<div v-if="product" class="subscribe-content">
<!-- 产品信息 -->
<div class="product-info-section">
<div class="section-title">
产品信息
</div>
<div class="product-info-card">
<div class="info-row">
<span class="label">产品名称</span>
<span class="value">{{ product.name }}</span>
</div>
<div class="info-row">
<span class="label">申购价格</span>
<span class="value price">¥{{ Number(product.price) }}</span>
</div>
</div>
</div>
<!-- 选择付款钱包 -->
<div class="wallet-section">
<div class="section-title">
选择付款钱包
</div>
<div class="wallet-list">
<div
v-for="wallet in wallets"
:key="wallet.id"
class="wallet-item"
:class="{ active: selectedWallet === wallet.walletTypeId }"
@click="selectedWallet = wallet.walletTypeId"
>
<div class="wallet-info">
<div class="wallet-name">
{{ wallet.walletType.name }}
</div>
<div class="wallet-balance">
可用余额¥{{ Number(wallet.available || 0).toFixed(2) }}
</div>
</div>
<div v-if="selectedWallet === wallet.walletTypeId" class="wallet-check">
<ion-icon :icon="checkmarkCircleOutline" />
</div>
</div>
</div>
</div>
<!-- 交易密码 -->
<div class="password-section">
<div class="section-title">
交易密码
</div>
<div class="password-input-wrapper">
<ion-input
v-model="paymentPassword"
type="password"
placeholder="请输入交易密码"
class="password-input"
inputmode="numeric"
:maxlength="20"
/>
</div>
<div class="password-hint">
交易密码用于验证申购操作请妥善保管
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<ion-button expand="block" fill="outline" @click="handleClose">
取消
</ion-button>
<ion-button expand="block" class="confirm-btn" @click="confirmSubscribe">
确认支付
</ion-button>
</div>
</div>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped>
.subscribe-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.product-info-section {
margin-bottom: 8px;
}
.product-info-card {
background: #fafafa;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.info-row .label {
font-size: 14px;
color: #999;
}
.info-row .value {
font-size: 15px;
font-weight: 600;
color: #333;
}
.info-row .value.price {
color: #c41e3a;
font-size: 18px;
}
.info-row .value.profit {
color: #52c41a;
font-size: 16px;
}
.wallet-section {
margin-bottom: 8px;
}
.wallet-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.wallet-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f5f5f5;
border: 2px solid transparent;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.wallet-item.active {
background: #fff7f0;
border-color: #c41e3a;
}
.wallet-item:active {
transform: scale(0.98);
}
.wallet-info {
flex: 1;
}
.wallet-name {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.wallet-balance {
font-size: 13px;
color: #666;
}
.wallet-check {
color: #c41e3a;
font-size: 24px;
}
.password-section {
margin-bottom: 8px;
}
.password-input-wrapper {
background: #f5f5f5;
border-radius: 12px;
padding: 4px 12px;
margin-bottom: 8px;
}
.password-input {
--padding-start: 8px;
--padding-end: 8px;
font-size: 16px;
}
.password-hint {
font-size: 12px;
color: #999;
line-height: 1.6;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 8px;
}
.action-buttons ion-button {
flex: 1;
--border-radius: 12px;
height: 48px;
font-weight: 600;
text-transform: none;
}
.action-buttons ion-button[fill="outline"] {
--border-color: #e8e8e8;
--color: #666;
}
.confirm-btn {
--background: linear-gradient(135deg, #c41e3a 0%, #8b1a2e 100%);
--background-activated: linear-gradient(135deg, #8b1a2e 0%, #6d1523 100%);
}
</style>

View File

@@ -2,18 +2,22 @@
import type { Treaty } from "@elysiajs/eden";
import type { InfiniteScrollCustomEvent } from "@ionic/vue";
import type { TreatyQuery } from "@/api/types";
import { modalController } from "@ionic/vue";
import { calendarOutline, cardOutline, timeOutline, trendingUpOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
import Subscribe from "./components/subscribe.vue";
type Product = Treaty.Data<typeof client.api.subscription.products.get>["data"][number];
type ProductQuery = TreatyQuery<typeof client.api.subscription.products.get>;
const modalInst = useTemplateRef<ModalInstance>("modalInst");
const [query] = useResetRef<ProductQuery>({
offset: 0,
limit: 10,
});
const data = ref<Product[]>([]);
const isFinished = ref(false);
const walletStore = useWalletStore();
async function fetchData() {
const { data: responseData } = await safeClient(client.api.subscription.products.get({ query: { ...query.value } }));
@@ -38,9 +42,25 @@ function handleProductClick(product: Product) {
// TODO: 跳转到产品详情
}
function handleSubscribe(product: Product) {
async function handleSubscribe(product: Product) {
console.log("申购产品:", product.name);
// TODO: 实现申购功能
await walletStore.syncWallets();
const wallets = walletStore.wallets.filter(w => w.walletType.allowTransaction === true);
const modal = await modalController.create({
component: Subscribe,
componentProps: {
productId: product.id,
wallets,
onClose: () => {
modal.dismiss();
},
onSuccess: () => {
modal.dismiss();
console.log("申购成功");
},
},
});
await modal.present();
}
onMounted(() => {
@@ -128,6 +148,8 @@ onMounted(() => {
<ion-icon slot="start" :icon="cardOutline" />
我要申购
</ion-button>
<ion-modal />
</div>
</div>
</div>