feat: 添加产品申购功能,集成钱包选择和交易密码验证
This commit is contained in:
3
auto-imports.d.ts
vendored
3
auto-imports.d.ts
vendored
@@ -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
6
components.d.ts
vendored
@@ -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']
|
||||
|
||||
4
src/utils/ionic-helper.ts
Normal file
4
src/utils/ionic-helper.ts
Normal 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>;
|
||||
322
src/views/product/components/subscribe.vue
Normal file
322
src/views/product/components/subscribe.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user