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'
|
export type { UploadFetchOptions } from './src/utils/aws/s3'
|
||||||
import('./src/utils/aws/s3')
|
import('./src/utils/aws/s3')
|
||||||
// @ts-ignore
|
// @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'
|
export type { Wallet } from './src/store/wallet'
|
||||||
import('./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']
|
IonItem: typeof import('@ionic/vue')['IonItem']
|
||||||
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
IonList: typeof import('@ionic/vue')['IonList']
|
IonList: typeof import('@ionic/vue')['IonList']
|
||||||
|
IonModal: typeof import('@ionic/vue')['IonModal']
|
||||||
IonPage: typeof import('@ionic/vue')['IonPage']
|
IonPage: typeof import('@ionic/vue')['IonPage']
|
||||||
|
IonRadio: typeof import('@ionic/vue')['IonRadio']
|
||||||
|
IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
|
||||||
IonRefresher: typeof import('@ionic/vue')['IonRefresher']
|
IonRefresher: typeof import('@ionic/vue')['IonRefresher']
|
||||||
IonRefresherContent: typeof import('@ionic/vue')['IonRefresherContent']
|
IonRefresherContent: typeof import('@ionic/vue')['IonRefresherContent']
|
||||||
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
||||||
@@ -72,7 +75,10 @@ declare global {
|
|||||||
const IonItem: typeof import('@ionic/vue')['IonItem']
|
const IonItem: typeof import('@ionic/vue')['IonItem']
|
||||||
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
const IonList: typeof import('@ionic/vue')['IonList']
|
const IonList: typeof import('@ionic/vue')['IonList']
|
||||||
|
const IonModal: typeof import('@ionic/vue')['IonModal']
|
||||||
const IonPage: typeof import('@ionic/vue')['IonPage']
|
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 IonRefresher: typeof import('@ionic/vue')['IonRefresher']
|
||||||
const IonRefresherContent: typeof import('@ionic/vue')['IonRefresherContent']
|
const IonRefresherContent: typeof import('@ionic/vue')['IonRefresherContent']
|
||||||
const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
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 { Treaty } from "@elysiajs/eden";
|
||||||
import type { InfiniteScrollCustomEvent } from "@ionic/vue";
|
import type { InfiniteScrollCustomEvent } from "@ionic/vue";
|
||||||
import type { TreatyQuery } from "@/api/types";
|
import type { TreatyQuery } from "@/api/types";
|
||||||
|
import { modalController } from "@ionic/vue";
|
||||||
import { calendarOutline, cardOutline, timeOutline, trendingUpOutline } from "ionicons/icons";
|
import { calendarOutline, cardOutline, timeOutline, trendingUpOutline } from "ionicons/icons";
|
||||||
import { client, safeClient } from "@/api";
|
import { client, safeClient } from "@/api";
|
||||||
|
import Subscribe from "./components/subscribe.vue";
|
||||||
|
|
||||||
type Product = Treaty.Data<typeof client.api.subscription.products.get>["data"][number];
|
type Product = Treaty.Data<typeof client.api.subscription.products.get>["data"][number];
|
||||||
type ProductQuery = TreatyQuery<typeof client.api.subscription.products.get>;
|
type ProductQuery = TreatyQuery<typeof client.api.subscription.products.get>;
|
||||||
|
|
||||||
|
const modalInst = useTemplateRef<ModalInstance>("modalInst");
|
||||||
const [query] = useResetRef<ProductQuery>({
|
const [query] = useResetRef<ProductQuery>({
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
});
|
});
|
||||||
const data = ref<Product[]>([]);
|
const data = ref<Product[]>([]);
|
||||||
const isFinished = ref(false);
|
const isFinished = ref(false);
|
||||||
|
const walletStore = useWalletStore();
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const { data: responseData } = await safeClient(client.api.subscription.products.get({ query: { ...query.value } }));
|
const { data: responseData } = await safeClient(client.api.subscription.products.get({ query: { ...query.value } }));
|
||||||
@@ -38,9 +42,25 @@ function handleProductClick(product: Product) {
|
|||||||
// TODO: 跳转到产品详情
|
// TODO: 跳转到产品详情
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubscribe(product: Product) {
|
async function handleSubscribe(product: Product) {
|
||||||
console.log("申购产品:", product.name);
|
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(() => {
|
onMounted(() => {
|
||||||
@@ -128,6 +148,8 @@ onMounted(() => {
|
|||||||
<ion-icon slot="start" :icon="cardOutline" />
|
<ion-icon slot="start" :icon="cardOutline" />
|
||||||
我要申购
|
我要申购
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|
||||||
|
<ion-modal />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user