Compare commits
10 Commits
3f9d3d3510
...
129fd3660e
| Author | SHA1 | Date | |
|---|---|---|---|
| 129fd3660e | |||
| 87cc321277 | |||
| d472a0d905 | |||
| 05a40f2062 | |||
| 7391240e88 | |||
| bf2aee8172 | |||
| 464db7f638 | |||
| 37dab6a506 | |||
| 6d440055ce | |||
| 76f286af83 |
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -52,8 +52,8 @@ catalogs:
|
||||
specifier: 8.0.0
|
||||
version: 8.0.0
|
||||
'@capp/eden':
|
||||
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.37.tgz
|
||||
version: 0.0.37
|
||||
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.41.tgz
|
||||
version: 0.0.41
|
||||
'@cloudflare/workers-types':
|
||||
specifier: ^4.20260113.0
|
||||
version: 4.20260116.0
|
||||
@@ -307,7 +307,7 @@ importers:
|
||||
version: 8.0.0(@capacitor/core@8.0.0)
|
||||
'@capp/eden':
|
||||
specifier: 'catalog:'
|
||||
version: http://192.168.1.2:9538/api/capp-eden-0.0.37.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))
|
||||
version: http://192.168.1.2:9538/api/capp-eden-0.0.41.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))
|
||||
'@elysiajs/eden':
|
||||
specifier: 'catalog:'
|
||||
version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||
@@ -1200,9 +1200,9 @@ packages:
|
||||
'@capacitor/synapse@1.0.4':
|
||||
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
|
||||
|
||||
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.37.tgz':
|
||||
resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.37.tgz}
|
||||
version: 0.0.37
|
||||
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.41.tgz':
|
||||
resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.41.tgz}
|
||||
version: 0.0.41
|
||||
peerDependencies:
|
||||
'@elysiajs/eden': ^1.4.6
|
||||
|
||||
@@ -6946,7 +6946,7 @@ snapshots:
|
||||
|
||||
'@capacitor/synapse@1.0.4': {}
|
||||
|
||||
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.37.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
||||
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.41.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))':
|
||||
dependencies:
|
||||
'@elysiajs/eden': 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ catalog:
|
||||
'@capacitor/share': ^8.0.0
|
||||
'@capacitor/status-bar': 8.0.0
|
||||
'@capacitor/toast': ^8.0.0
|
||||
'@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.37.tgz
|
||||
'@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.41.tgz
|
||||
'@cloudflare/workers-types': ^4.20260113.0
|
||||
'@elysiajs/eden': ^1.4.6
|
||||
'@faker-js/faker': ^10.2.0
|
||||
|
||||
@@ -39,6 +39,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/product/records",
|
||||
component: () => import("@/views/product/records.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/news/:id",
|
||||
props: true,
|
||||
|
||||
@@ -23,8 +23,8 @@ const [investmentQuery] = useResetRef<any>({
|
||||
});
|
||||
|
||||
const { data: allRecords, execute: executeAll } = safeClient(() => client.api.ledger.entries.get({ query: { ...allQuery.value } }));
|
||||
const { data: incomeRecords, execute: executeIncome } = safeClient(() => client.api.ledger.entries.get({ query: { ...incomeQuery.value } }));
|
||||
const { data: investmentRecords, execute: executeInvestment } = safeClient(() => client.api.ledger.entries.get({ query: { ...investmentQuery.value } }));
|
||||
const { data: incomeRecords, execute: executeIncome } = safeClient(() => client.api.ledger.entries.get({ query: { ...incomeQuery.value } }), { immediate: false });
|
||||
const { data: investmentRecords, execute: executeInvestment } = safeClient(() => client.api.ledger.entries.get({ query: { ...investmentQuery.value } }), { immediate: false });
|
||||
|
||||
// 当前选中的标签
|
||||
const selectedTab = ref<"all" | "income" | "investment">("all");
|
||||
|
||||
@@ -9,8 +9,7 @@ export const SignupSchema = zod.object({
|
||||
password: zod
|
||||
.string()
|
||||
.min(6, "密码至少6位")
|
||||
.max(20, "密码最多20位")
|
||||
.regex(/^(?=.*[a-z])(?=.*\d).+$/i, "密码必须包含字母和数字"),
|
||||
.max(20, "密码最多20位"),
|
||||
|
||||
confirmPassword: zod
|
||||
.string()
|
||||
|
||||
@@ -34,7 +34,9 @@ function isCheckedIn(day: number) {
|
||||
}
|
||||
|
||||
async function handleSignup() {
|
||||
await safeClient(client.api.checkIns.post());
|
||||
await safeClient(client.api.checkIns.post({
|
||||
checkInType: "app",
|
||||
}));
|
||||
loadData();
|
||||
toastController.create({
|
||||
message: "签到成功!",
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { bagHandleOutline, calendarOutline, cardOutline, timeOutline, trendingUpOutline } from "ionicons/icons";
|
||||
import { client, safeClient } from "@/api";
|
||||
import Subscribe from "./components/subscribe.vue";
|
||||
|
||||
@@ -82,13 +82,17 @@ onMounted(() => {
|
||||
<!-- 基金产品列表 -->
|
||||
<section class="mb-5 -mt-5 ion-padding-horizontal">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex justify-between items-center mb-4 w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="@/assets/images/icon.png" class="size-7">
|
||||
<div class="text-xl font-bold text-[#1a1a1a]">
|
||||
基金产品
|
||||
</div>
|
||||
</div>
|
||||
<ion-button fill="clear" size="small" @click="$router.push('/product/records')">
|
||||
<ion-icon slot="start" :icon="bagHandleOutline" />
|
||||
我的业务
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,6 +141,12 @@ onMounted(() => {
|
||||
{{ Number(product.cycleDays) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs text-[#999] mb-1">改革先锋扶持金</span>
|
||||
<span class="text-lg font-bold text-[#52c41a] flex items-center gap-0.5">
|
||||
¥{{ Number(product.reformPioneerAllowance) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 申购按钮 -->
|
||||
@@ -154,6 +164,13 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ion-infinite-scroll threshold="100px" @ion-infinite="handleInfinite">
|
||||
<ion-infinite-scroll-content
|
||||
loading-spinner="bubbles"
|
||||
loading-text="加载更多产品..."
|
||||
/>
|
||||
</ion-infinite-scroll>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
128
src/views/product/records.vue
Normal file
128
src/views/product/records.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script lang='ts' setup>
|
||||
import type { Treaty } from "@elysiajs/eden";
|
||||
import type { InfiniteScrollCustomEvent } from "@ionic/vue";
|
||||
import type { TreatyQuery } from "@/api/types";
|
||||
import { client, safeClient } from "@/api";
|
||||
|
||||
type Subscribe = Treaty.Data<typeof client.api.subscription.orders.get>["data"][number];
|
||||
type SubscribeQuery = TreatyQuery<typeof client.api.subscription.orders.get>;
|
||||
|
||||
const [query] = useResetRef<SubscribeQuery>({
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
});
|
||||
const data = ref<Subscribe[]>([]);
|
||||
const isFinished = ref(false);
|
||||
|
||||
async function fetchData() {
|
||||
const { data: responseData } = await safeClient(client.api.subscription.orders.get({ query: { ...query.value } }));
|
||||
data.value.push(...(responseData.value?.data || []));
|
||||
isFinished.value = responseData.value?.pagination.hasNextPage === false;
|
||||
}
|
||||
async function handleInfinite(event: InfiniteScrollCustomEvent) {
|
||||
if (isFinished.value) {
|
||||
event.target.complete();
|
||||
event.target.disabled = true;
|
||||
return;
|
||||
}
|
||||
query.value.offset! += query.value.limit!;
|
||||
await fetchData();
|
||||
setTimeout(() => {
|
||||
event.target.complete();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</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>我的业务</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content :fullscreen="true">
|
||||
<div class="p-4">
|
||||
<empty v-if="data.length === 0 && !isFinished" class="my-20" />
|
||||
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="item in data"
|
||||
:key="item.id"
|
||||
class="bg-white rounded-2xl p-4 shadow-sm"
|
||||
>
|
||||
<!-- 顶部状态栏 -->
|
||||
<div class="flex items-center justify-between mb-3 pb-3 border-b border-gray-100">
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ useDateFormat(item.createdAt, 'YYYY-MM-DD HH:mm') }}
|
||||
</div>
|
||||
<div
|
||||
class="px-3 py-1 rounded-full text-xs font-semibold"
|
||||
:class="{
|
||||
'bg-green-50 text-green-600': item.status === 'unlocked',
|
||||
'bg-blue-50 text-blue-600': item.status === 'locked',
|
||||
'bg-yellow-50 text-yellow-600': item.status === 'pending',
|
||||
'bg-red-50 text-red-600': item.status === 'cancelled',
|
||||
'bg-gray-50 text-gray-600': !['unlocked', 'locked', 'pending', 'cancelled'].includes(item.status),
|
||||
}"
|
||||
>
|
||||
{{ item.status === 'unlocked' ? '收益已发放' : item.status === 'locked' ? '认购成功' : item.status === 'pending' ? '待处理' : item.status === 'cancelled' ? '已取消' : item.status }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 金额信息 -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-3">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">
|
||||
申购金额
|
||||
</div>
|
||||
<div class="text-lg font-bold text-[#c41e3a]">
|
||||
¥{{ Number(item.price).toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 mb-1">
|
||||
改革先锋津贴
|
||||
</div>
|
||||
<div class="text-lg font-bold text-green-600">
|
||||
¥{{ Number(item.reformPioneerAllowance).toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 周期信息 -->
|
||||
<div class="flex items-center justify-between pt-3 border-t border-gray-100">
|
||||
<div class="text-sm text-gray-600">
|
||||
投资周期
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-gray-900">
|
||||
{{ item.cycleDays }} 天
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-infinite-scroll threshold="100px" :disabled="isFinished" @ion-infinite="handleInfinite">
|
||||
<ion-infinite-scroll-content
|
||||
loading-spinner="bubbles"
|
||||
loading-text="加载更多..."
|
||||
/>
|
||||
</ion-infinite-scroll>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped>
|
||||
.shadow-sm {
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang='ts' setup>
|
||||
import { alertController, toastController } from "@ionic/vue";
|
||||
import { cardOutline, checkmarkCircleOutline, documentTextOutline, logoAlipay, logoWechat, walletOutline } from "ionicons/icons";
|
||||
import { safeClient } from "@/api";
|
||||
import { client, safeClient } from "@/api";
|
||||
|
||||
const walletStore = useWalletStore();
|
||||
const balanceWallet = await walletStore.getWalletByType("balance");
|
||||
@@ -99,36 +99,26 @@ function goToRecords() {
|
||||
router.push("/recharge/records");
|
||||
}
|
||||
|
||||
async function showToast(message: string, color: "success" | "danger" | "warning" = "success") {
|
||||
const toast = await toastController.create({
|
||||
message,
|
||||
duration: 2000,
|
||||
position: "top",
|
||||
color,
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const amount = Number.parseFloat(rechargeAmount.value);
|
||||
|
||||
if (!rechargeAmount.value || Number.isNaN(amount) || amount <= 0) {
|
||||
await showToast("请输入有效的充值金额", "warning");
|
||||
showToast("请输入有效的充值金额");
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount < 10) {
|
||||
await showToast("充值金额不能低于10元", "warning");
|
||||
showToast("充值金额不能低于10元");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPaymentType.value) {
|
||||
await showToast("请选择支付方式", "warning");
|
||||
showToast("请选择支付方式");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPaymentId.value) {
|
||||
await showToast("请选择支付账号", "warning");
|
||||
showToast("请选择支付账号");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,13 +133,17 @@ async function handleSubmit() {
|
||||
{
|
||||
text: "确认",
|
||||
handler: async () => {
|
||||
// TODO: 调用充值 API
|
||||
await showToast("充值成功");
|
||||
await safeClient(client.api.deposit.fiat.post({
|
||||
amount: String(amount),
|
||||
paymentChannel: selectedPaymentType.value!,
|
||||
}));
|
||||
showToast("充值申请已提交,请等待处理");
|
||||
rechargeAmount.value = "";
|
||||
selectedPaymentType.value = null;
|
||||
selectedPaymentId.value = null;
|
||||
const data = await walletStore.getWalletByType("balance");
|
||||
balanceWallet.value = data.value;
|
||||
// TODO 刷新余额
|
||||
// const data = await walletStore.getWalletByType("balance");
|
||||
// balanceWallet.value = data.value;
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -169,7 +163,7 @@ async function handleSubmit() {
|
||||
<ion-title>充值</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button color="light" size="small" @click="goToRecords">
|
||||
<ion-icon slot="icon-only" :icon="documentTextOutline" />
|
||||
充值记录
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
<script lang='ts' setup>
|
||||
import type { Treaty } from "@elysiajs/eden";
|
||||
import type { InfiniteScrollCustomEvent } from "@ionic/vue";
|
||||
import type { TreatyQuery } from "@/api/types";
|
||||
import { cardOutline, listOutline, logoAlipay, logoWechat } from "ionicons/icons";
|
||||
import { client, safeClient } from "@/api";
|
||||
|
||||
interface RechargeRecord {
|
||||
id: number;
|
||||
amount: number;
|
||||
paymentType: "alipay" | "unionpay" | "wechat";
|
||||
paymentAccount: string;
|
||||
status: "success" | "pending" | "failed";
|
||||
time: string;
|
||||
orderNo: string;
|
||||
type DepositRecord = Treaty.Data<typeof client.api.deposit.orders.get>["data"][number];
|
||||
type DepositQuery = TreatyQuery<typeof client.api.deposit.orders.get>;
|
||||
|
||||
const [query] = useResetRef<DepositQuery>({
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
});
|
||||
const data = ref<DepositRecord[]>([]);
|
||||
const isFinished = ref(false);
|
||||
const walletStore = useWalletStore();
|
||||
|
||||
async function fetchData() {
|
||||
const { data: responseData } = await safeClient(client.api.deposit.orders.get({ query: { ...query.value } }));
|
||||
data.value.push(...(responseData.value?.data || []));
|
||||
isFinished.value = responseData.value?.pagination.hasNextPage === false;
|
||||
}
|
||||
async function handleInfinite(event: InfiniteScrollCustomEvent) {
|
||||
if (isFinished.value) {
|
||||
event.target.complete();
|
||||
event.target.disabled = true;
|
||||
return;
|
||||
}
|
||||
query.value.offset! += query.value.limit!;
|
||||
await fetchData();
|
||||
setTimeout(() => {
|
||||
event.target.complete();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 支付方式选项
|
||||
@@ -18,121 +41,42 @@ const paymentTypes = [
|
||||
{ type: "wechat" as const, name: "微信", icon: logoWechat, color: "#07C160" },
|
||||
];
|
||||
|
||||
// 充值记录数据
|
||||
const rechargeRecords = ref<RechargeRecord[]>([
|
||||
{
|
||||
id: 1,
|
||||
amount: 1000.00,
|
||||
paymentType: "alipay",
|
||||
paymentAccount: "138****8000",
|
||||
status: "success",
|
||||
time: "2026-01-18 14:30:20",
|
||||
orderNo: "RCH202601181430001",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
amount: 500.00,
|
||||
paymentType: "wechat",
|
||||
paymentAccount: "wxid_abc123",
|
||||
status: "success",
|
||||
time: "2026-01-17 10:15:30",
|
||||
orderNo: "RCH202601171015002",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
amount: 2000.00,
|
||||
paymentType: "unionpay",
|
||||
paymentAccount: "6222 **** **** 1234",
|
||||
status: "pending",
|
||||
time: "2026-01-16 16:45:10",
|
||||
orderNo: "RCH202601161645003",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
amount: 100.00,
|
||||
paymentType: "alipay",
|
||||
paymentAccount: "159****6666",
|
||||
status: "success",
|
||||
time: "2026-01-15 09:20:00",
|
||||
orderNo: "RCH202601150920004",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
amount: 800.00,
|
||||
paymentType: "unionpay",
|
||||
paymentAccount: "6217 **** **** 5678",
|
||||
status: "failed",
|
||||
time: "2026-01-14 20:30:15",
|
||||
orderNo: "RCH202601142030005",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
amount: 300.00,
|
||||
paymentType: "alipay",
|
||||
paymentAccount: "138****8000",
|
||||
status: "success",
|
||||
time: "2026-01-13 11:45:30",
|
||||
orderNo: "RCH202601131145006",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
amount: 1500.00,
|
||||
paymentType: "wechat",
|
||||
paymentAccount: "wxid_abc123",
|
||||
status: "success",
|
||||
time: "2026-01-12 15:20:10",
|
||||
orderNo: "RCH202601121520007",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
amount: 600.00,
|
||||
paymentType: "unionpay",
|
||||
paymentAccount: "6222 **** **** 1234",
|
||||
status: "success",
|
||||
time: "2026-01-11 09:30:45",
|
||||
orderNo: "RCH202601110930008",
|
||||
},
|
||||
]);
|
||||
|
||||
function getPaymentTypeName(type: "alipay" | "unionpay" | "wechat") {
|
||||
function getPaymentTypeName(type: string | null) {
|
||||
return paymentTypes.find(pt => pt.type === type)?.name || type;
|
||||
}
|
||||
|
||||
function getPaymentTypeIcon(type: "alipay" | "unionpay" | "wechat") {
|
||||
function getPaymentTypeIcon(type: string | null) {
|
||||
return paymentTypes.find(pt => pt.type === type)?.icon || cardOutline;
|
||||
}
|
||||
|
||||
function getPaymentTypeColor(type: "alipay" | "unionpay" | "wechat") {
|
||||
function getPaymentTypeColor(type: string | null) {
|
||||
return paymentTypes.find(pt => pt.type === type)?.color || "#999";
|
||||
}
|
||||
|
||||
function getStatusText(status: "success" | "pending" | "failed") {
|
||||
function getStatusText(status: DepositRecord["status"]) {
|
||||
const statusMap = {
|
||||
success: "成功",
|
||||
pending: "处理中",
|
||||
failed: "失败",
|
||||
pending: "待处理",
|
||||
approved: "已批准",
|
||||
completed: "已完成",
|
||||
rejected: "已拒绝",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
return statusMap[status];
|
||||
}
|
||||
|
||||
function getStatusColor(status: "success" | "pending" | "failed") {
|
||||
function getStatusColor(status: DepositRecord["status"]) {
|
||||
const colorMap = {
|
||||
success: "#52c41a",
|
||||
pending: "#faad14",
|
||||
failed: "#f5222d",
|
||||
approved: "#52c41a",
|
||||
completed: "#52c41a",
|
||||
rejected: "#f5222d",
|
||||
cancelled: "#f5222d",
|
||||
};
|
||||
return colorMap[status];
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const totalAmount = computed(() => {
|
||||
return rechargeRecords.value
|
||||
.filter(r => r.status === "success")
|
||||
.reduce((sum, r) => sum + r.amount, 0);
|
||||
});
|
||||
|
||||
const totalCount = computed(() => {
|
||||
return rechargeRecords.value.filter(r => r.status === "success").length;
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -148,50 +92,27 @@ const totalCount = computed(() => {
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
累计充值金额
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
¥{{ totalAmount.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-divider" />
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
充值成功次数
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
{{ totalCount }}次
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 充值记录列表 -->
|
||||
<div class="records-section">
|
||||
<div v-if="rechargeRecords.length > 0" class="records-list">
|
||||
<div v-if="data.length > 0" class="records-list">
|
||||
<div
|
||||
v-for="record in rechargeRecords"
|
||||
v-for="record in data"
|
||||
:key="record.id"
|
||||
class="record-item"
|
||||
>
|
||||
<div class="record-left">
|
||||
<ion-icon
|
||||
:icon="getPaymentTypeIcon(record.paymentType)"
|
||||
:icon="getPaymentTypeIcon(record.paymentChannel)"
|
||||
class="record-icon"
|
||||
:style="{ color: getPaymentTypeColor(record.paymentType) }"
|
||||
:style="{ color: getPaymentTypeColor(record.paymentChannel) }"
|
||||
/>
|
||||
<div class="record-info">
|
||||
<div class="record-title">
|
||||
{{ getPaymentTypeName(record.paymentType) }}充值
|
||||
</div>
|
||||
<div class="record-account">
|
||||
{{ record.paymentAccount }}
|
||||
{{ getPaymentTypeName(record.paymentChannel) }}充值
|
||||
</div>
|
||||
|
||||
<div class="record-time">
|
||||
{{ record.time }}
|
||||
{{ useDateFormat(record.createdAt, 'YYYY-MM-DD HH:mm:ss') }}
|
||||
</div>
|
||||
<div class="record-order">
|
||||
订单号:{{ record.orderNo }}
|
||||
@@ -200,7 +121,7 @@ const totalCount = computed(() => {
|
||||
</div>
|
||||
<div class="record-right">
|
||||
<div class="record-amount">
|
||||
+¥{{ record.amount.toFixed(2) }}
|
||||
+¥{{ Number(record.amount) }}
|
||||
</div>
|
||||
<div
|
||||
class="record-status"
|
||||
@@ -219,6 +140,13 @@ const totalCount = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-infinite-scroll threshold="100px" @ion-infinite="handleInfinite">
|
||||
<ion-infinite-scroll-content
|
||||
loading-spinner="bubbles"
|
||||
loading-text="加载更多记录..."
|
||||
/>
|
||||
</ion-infinite-scroll>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang='ts' setup>
|
||||
import { toastController } from "@ionic/vue";
|
||||
import { eyeOffOutline, eyeOutline, lockClosedOutline } from "ionicons/icons";
|
||||
import zod from "zod";
|
||||
import { safeClient } from "@/api";
|
||||
@@ -17,6 +16,7 @@ const showCurrentPassword = ref(false);
|
||||
const showNewPassword = ref(false);
|
||||
const showConfirmPassword = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 表单验证 Schema
|
||||
const ChangePasswordSchema = zod.object({
|
||||
@@ -27,8 +27,7 @@ const ChangePasswordSchema = zod.object({
|
||||
newPassword: zod
|
||||
.string()
|
||||
.min(6, "新密码至少6位")
|
||||
.max(20, "新密码最多20位")
|
||||
.regex(/^(?=.*[a-z])(?=.*\d).+$/i, "新密码必须包含字母和数字"),
|
||||
.max(20, "新密码最多20位"),
|
||||
|
||||
confirmPassword: zod
|
||||
.string()
|
||||
@@ -41,16 +40,6 @@ const ChangePasswordSchema = zod.object({
|
||||
path: ["newPassword"],
|
||||
});
|
||||
|
||||
async function showToast(message: string, color: "success" | "danger" | "warning" = "danger") {
|
||||
const toast = await toastController.create({
|
||||
message,
|
||||
duration: 2000,
|
||||
position: "top",
|
||||
color,
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const result = ChangePasswordSchema.safeParse(formData.value);
|
||||
if (!result.success) {
|
||||
@@ -66,12 +55,12 @@ async function handleSubmit() {
|
||||
currentPassword: formData.value.currentPassword, // required
|
||||
revokeOtherSessions: true,
|
||||
}));
|
||||
|
||||
await showToast("登录密码修改成功,请使用新密码登录", "success");
|
||||
await showToast("登录密码修改成功,请使用新密码登录");
|
||||
userStore.signOut(); // 修改密码后登出
|
||||
router.back();
|
||||
}
|
||||
catch (error) {
|
||||
await showToast("密码修改失败,请检查当前密码是否正确", "danger");
|
||||
await showToast("密码修改失败,请检查当前密码是否正确");
|
||||
}
|
||||
finally {
|
||||
isSubmitting.value = false;
|
||||
@@ -246,16 +235,16 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
--placeholder-color: #999;
|
||||
--placeholder-opacity: 1;
|
||||
font-size: 15px;
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
font-size: 16px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
|
||||
@@ -19,10 +19,11 @@ const [depth3Query] = useResetRef({
|
||||
pageSize: 10,
|
||||
});
|
||||
// 统计数据
|
||||
const { data: statistics } = safeClient(client.api.team.stats.get());
|
||||
const { data: statistics } = safeClient(client.api.team.dashboard.summary.get());
|
||||
const { data: earnings } = safeClient(client.api.team.earnings.get());
|
||||
const { data: depth1, execute: executeDepth1 } = safeClient(() => client.api.team.members.get({ query: { ...depth1Query.value } }));
|
||||
const { data: depth2, execute: executeDepth2 } = safeClient(() => client.api.team.members.get({ query: { ...depth2Query.value } }));
|
||||
const { data: depth3, execute: executeDepth3 } = safeClient(() => client.api.team.members.get({ query: { ...depth3Query.value } }));
|
||||
const { data: depth2, execute: executeDepth2 } = safeClient(() => client.api.team.members.get({ query: { ...depth2Query.value } }), { immediate: false });
|
||||
const { data: depth3, execute: executeDepth3 } = safeClient(() => client.api.team.members.get({ query: { ...depth3Query.value } }), { immediate: false });
|
||||
|
||||
// 当前选中的级别
|
||||
const selectedLevel = ref<1 | 2 | 3>(1);
|
||||
@@ -133,91 +134,158 @@ function nextPage() {
|
||||
<ion-icon :icon="peopleOutline" class="title-icon" />
|
||||
<span>团队规模</span>
|
||||
</div>
|
||||
<div class="team-size-card">
|
||||
<div class="team-total">
|
||||
<div class="total-value">
|
||||
{{ statistics?.totalCount }}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-value">
|
||||
{{ statistics?.totalCount || 0 }}
|
||||
</div>
|
||||
<div class="total-label">
|
||||
<div class="stat-label">
|
||||
团队总人数
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-growth">
|
||||
<div class="growth-item">
|
||||
<span class="growth-label">今日新增</span>
|
||||
<span class="growth-value">{{ statistics?.todayCount }}</span>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value small">
|
||||
{{ statistics?.todayCount || 0 }}
|
||||
</div>
|
||||
<div class="growth-item">
|
||||
<span class="growth-label">昨日新增</span>
|
||||
<span class="growth-value">{{ statistics?.yesterdayCount }}</span>
|
||||
<div class="stat-label">
|
||||
今日新增
|
||||
</div>
|
||||
<div class="growth-item">
|
||||
<span class="growth-label">本月新增</span>
|
||||
<span class="growth-value">{{ statistics?.monthCount }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value small">
|
||||
{{ statistics?.monthCount || 0 }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
本月新增
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid mt-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value small">
|
||||
{{ statistics?.totalRegister || 0 }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
总注册
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value small">
|
||||
{{ statistics?.totalKyc || 0 }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
总实名
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value small">
|
||||
{{ statistics?.totalActivated || 0 }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
总激活
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 助力统计 -->
|
||||
<!-- <div class="stats-section">
|
||||
<div class="section-title">
|
||||
<ion-icon :icon="trendingUpOutline" class="title-icon" />
|
||||
<span>团队助力</span>
|
||||
</div>
|
||||
<div class="unified-card">
|
||||
<div class="unified-left">
|
||||
<div class="unified-value">
|
||||
¥{{ statistics?.totalSupport.toFixed(2) }}
|
||||
</div>
|
||||
<div class="unified-label">
|
||||
团队总助力
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-right">
|
||||
<div class="unified-item">
|
||||
<span class="unified-item-label">本月助力</span>
|
||||
<span class="unified-item-value">¥{{ statistics?.monthSupport.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="unified-item">
|
||||
<span class="unified-item-label">较上月</span>
|
||||
<span class="unified-item-value" :class="supportChange.isPositive ? 'success' : 'danger'">
|
||||
{{ supportChange.isPositive ? '+' : '' }}{{ supportChange.percent }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 收益统计 -->
|
||||
<!-- <div class="stats-section">
|
||||
<div class="stats-section">
|
||||
<div class="section-title">
|
||||
<ion-icon :icon="walletOutline" class="title-icon" />
|
||||
<span>团队收益</span>
|
||||
<span>团队助力</span>
|
||||
</div>
|
||||
<div class="unified-card">
|
||||
<div class="unified-left">
|
||||
<div class="unified-value">
|
||||
¥{{ statistics?.todayIncome.toFixed(2) }}
|
||||
|
||||
<!-- 总助力 -->
|
||||
<div class="earnings-main-card">
|
||||
<div class="earnings-total">
|
||||
<div class="earnings-label">
|
||||
团队总助力
|
||||
</div>
|
||||
<div class="unified-label">
|
||||
今日收益
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-right">
|
||||
<div class="unified-item">
|
||||
<span class="unified-item-label">本周收益</span>
|
||||
<span class="unified-item-value">¥{{ statistics.weekIncome.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="unified-item">
|
||||
<span class="unified-item-label">较上月</span>
|
||||
<span class="unified-item-value" :class="incomeChange.isPositive ? 'success' : 'danger'">
|
||||
{{ incomeChange.isPositive ? '+' : '' }}{{ incomeChange.percent }}%
|
||||
</span>
|
||||
<div class="earnings-value">
|
||||
¥{{ Number(earnings?.totalAmount || 0).toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 本月数据 -->
|
||||
<div class="earnings-section">
|
||||
<div class="earnings-section-title">
|
||||
本月助力
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card accent">
|
||||
<div class="stat-value small">
|
||||
¥{{ Number(earnings?.monthAmount || 0).toFixed(2) }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
本月金额
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value small">
|
||||
¥{{ Number(earnings?.lastMonthAmount || 0).toFixed(2) }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
上月金额
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" :class="Number(earnings?.monthIncreaseAmount || 0) >= 0 ? 'success' : 'danger'">
|
||||
<div class="stat-value small">
|
||||
{{ Number(earnings?.monthIncreaseAmount || 0) >= 0 ? '+' : '' }}¥{{ Number(earnings?.monthIncreaseAmount || 0).toFixed(2) }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
月增长
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本周数据 -->
|
||||
<div class="earnings-section">
|
||||
<div class="earnings-section-title">
|
||||
本周助力
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card accent">
|
||||
<div class="stat-value small">
|
||||
¥{{ Number(earnings?.weekAmount || 0).toFixed(2) }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
本周金额
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value small">
|
||||
¥{{ Number(earnings?.lastWeekAmount || 0).toFixed(2) }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
上周金额
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" :class="Number(earnings?.weekIncreaseAmount || 0) >= 0 ? 'success' : 'danger'">
|
||||
<div class="stat-value small">
|
||||
{{ Number(earnings?.weekIncreaseAmount || 0) >= 0 ? '+' : '' }}¥{{ Number(earnings?.weekIncreaseAmount || 0).toFixed(2) }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
周增长
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 今日数据 -->
|
||||
<div class="today-earnings-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-gray-600">
|
||||
今日助力
|
||||
</div>
|
||||
<div class="text-xl font-bold text-[#c41e3a]">
|
||||
¥{{ Number(earnings?.todayAmount || 0).toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 团队列表 -->
|
||||
<div class="team-list-section">
|
||||
@@ -328,6 +396,7 @@ function nextPage() {
|
||||
overflow: hidden;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
@@ -349,113 +418,116 @@ function nextPage() {
|
||||
color: #c41e3a;
|
||||
}
|
||||
|
||||
.team-size-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #fff7f0 0%, #ffe8e8 100%);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.team-total {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #c41e3a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.team-growth {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.growth-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.growth-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.growth-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.unified-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e8edf3 100%);
|
||||
.stat-card {
|
||||
background: #f5f7fa;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.unified-left {
|
||||
flex: 1;
|
||||
padding: 16px 12px;
|
||||
text-align: center;
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid #333;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.unified-value {
|
||||
.stat-card.primary {
|
||||
background: linear-gradient(135deg, #c41e3a 0%, #8b1a2e 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-card.accent {
|
||||
background: linear-gradient(135deg, #fff7f0 0%, #ffe8e8 100%);
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
||||
}
|
||||
|
||||
.stat-card.danger {
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-card.primary .stat-value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-card.accent .stat-value {
|
||||
color: #c41e3a;
|
||||
}
|
||||
|
||||
.stat-card.success .stat-value {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.stat-card.danger .stat-value {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.stat-value.small {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-card.primary .stat-label {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.earnings-main-card {
|
||||
background: linear-gradient(135deg, #c41e3a 0%, #8b1a2e 100%);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.earnings-total {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.earnings-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.unified-label {
|
||||
.earnings-value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.earnings-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.earnings-section:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.earnings-section-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.unified-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.unified-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.unified-item-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.unified-item-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 12px;
|
||||
border-left: 3px solid #c41e3a;
|
||||
}
|
||||
|
||||
.unified-item-value.success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.unified-item-value.danger {
|
||||
color: #ff4d4f;
|
||||
.today-earnings-card {
|
||||
background: linear-gradient(135deg, #fff7f0 0%, #ffe8e8 100%);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.team-list-section {
|
||||
|
||||
Reference in New Issue
Block a user