feat: 添加绑定邀请码功能,支持邀请码输入和邀请人员列表展示

This commit is contained in:
2026-01-18 18:07:38 +07:00
parent 3be2362610
commit 7a17102141
3 changed files with 527 additions and 23 deletions

View File

@@ -49,6 +49,11 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("@/views/invite/index.vue"),
meta: { requiresAuth: true },
},
{
path: "/bind_invite",
component: () => import("@/views/invite/bind.vue"),
meta: { requiresAuth: true },
},
{
path: "/settings",
component: () => import("@/views/settings/index.vue"),

502
src/views/invite/bind.vue Normal file
View File

@@ -0,0 +1,502 @@
<script lang='ts' setup>
import { Clipboard } from "@capacitor/clipboard";
import { onIonViewWillEnter, toastController } from "@ionic/vue";
import { checkmarkCircleOutline, copyOutline, downloadOutline, linkOutline, ticketOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
const route = useRoute();
const userStore = useUserStore();
const { userProfile } = storeToRefs(userStore);
// 绑定邀请码相关
const bindReferralCode = ref("");
const isBinding = ref(false);
const copyStatus = ref({
link: false,
code: false,
});
// 邀请人员列表
const [inviteQuery] = useResetRef({
pageIndex: 1,
pageSize: 10,
maxDepth: 1,
});
const { data: inviteList, execute: executeInviteList } = safeClient(() =>
client.api.referrals.members.get({ query: { ...inviteQuery.value } }),
);
const invitedMembers = computed(() => inviteList.value?.data || []);
const totalInvites = computed(() => inviteList.value?.pagination.total || 0);
const totalPages = computed(() => Math.ceil(totalInvites.value / inviteQuery.value.pageSize));
const currentPage = computed(() => inviteQuery.value.pageIndex);
async function handleCopy(text?: string, type: "link" | "code" | "download" = "link") {
if (!text) {
console.warn("没有可复制的文本");
return;
}
try {
await Clipboard.write({
string: text,
});
copyStatus.value[type] = true;
setTimeout(() => {
copyStatus.value[type] = false;
}, 2000);
console.log(`已复制${type}:`, text);
}
catch (error) {
console.error("复制失败:", error);
try {
await navigator.clipboard.writeText(text);
copyStatus.value[type] = true;
setTimeout(() => {
copyStatus.value[type] = false;
}, 2000);
}
catch (err) {
console.error("浏览器复制也失败:", err);
}
}
}
function handleDownloadQR() {
console.log("下载二维码");
// TODO: 实现下载二维码功能
}
// 从路由query获取邀请码
onIonViewWillEnter(() => {
const code = route.query.code || route.query.referralCode;
if (code && typeof code === "string") {
bindReferralCode.value = code;
}
// 加载邀请人员列表
executeInviteList();
});
// 绑定邀请码
async function handleBindReferralCode() {
if (!bindReferralCode.value.trim()) {
const toast = await toastController.create({
message: "请输入邀请码",
duration: 2000,
color: "warning",
});
await toast.present();
return;
}
if (bindReferralCode.value.trim().length !== 6) {
const toast = await toastController.create({
message: "邀请码格式不正确请输入6位邀请码",
duration: 2000,
color: "warning",
});
await toast.present();
return;
}
try {
isBinding.value = true;
await safeClient(
client.api.referrals.bind.post({
referralCode: bindReferralCode.value.trim(),
}),
);
const toast = await toastController.create({
message: "绑定成功!",
duration: 2000,
color: "success",
});
await toast.present();
bindReferralCode.value = "";
await userStore.updateProfile();
}
finally {
isBinding.value = false;
}
}
// 分页功能
async function goToPage(page: number) {
if (page >= 1 && page <= totalPages.value) {
inviteQuery.value.pageIndex = page;
await executeInviteList();
}
}
function prevPage() {
goToPage(currentPage.value - 1);
}
function nextPage() {
goToPage(currentPage.value + 1);
}
</script>
<template>
<ion-page>
<ion-content :fullscreen="true">
<!-- 顶部横幅 -->
<img src="@/assets/images/invite-banner.jpg" class="w-full object-cover" alt="邀请横幅">
<div class="ion-padding-horizontal">
<!-- 绑定邀请码 -->
<section v-if="!userProfile?.referredBy" class="mb-5 mt-5">
<div class="bind-card rounded-2xl p-5">
<div class="flex items-center gap-2 mb-3">
<ion-icon :icon="linkOutline" class="text-2xl text-[#c41e3a]" />
<div class="text-lg font-bold text-[#1a1a1a]">
绑定邀请码
</div>
</div>
<p class="text-sm text-[#666] mb-4">
输入好友的邀请码绑定后双方均可获得奖励
</p>
<div class="flex gap-3">
<ion-input
v-model="bindReferralCode"
placeholder="请输入6位邀请码"
:maxlength="6"
class="bind-input"
:disabled="isBinding"
/>
<ion-button
expand="block"
class="bind-submit-btn"
:disabled="isBinding || !bindReferralCode.trim()"
@click="handleBindReferralCode"
>
<ion-spinner v-if="isBinding" name="crescent" />
<span v-else>绑定</span>
</ion-button>
</div>
</div>
</section>
<section class="mb-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<img src="@/assets/images/icon.png" class="size-7">
<div class="text-lg font-bold text-[#1a1a1a]">
我的邀请人员
</div>
</div>
<div class="text-sm text-[#666]">
<span class="text-[#c41e3a] font-semibold">{{ totalInvites }}</span>
</div>
</div>
<!-- 邀请人员列表 -->
<div v-if="invitedMembers.length > 0" class="invite-list">
<div
v-for="(member, index) in invitedMembers"
:key="index"
class="invite-member-card"
>
<div class="member-header">
<div class="member-avatar">
{{ member.profile.avatar }}
</div>
<div class="member-info">
<div class="member-name">
{{ member.profile.nickname }}
</div>
<div class="member-phone">
{{ member.profile.uid }}
</div>
</div>
</div>
<div class="member-details">
<div class="detail-item">
<span class="detail-label">加入时间</span>
<span class="detail-value">{{ member?.joinTime }}</span>
</div>
<div class="detail-item">
<span class="detail-label">投资金额</span>
<span class="detail-value amount">¥{{ member?.investAmount?.toFixed(2) || '0.00' }}</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-invite-list">
<ion-icon :icon="ticketOutline" class="empty-icon" />
<div class="empty-text">
暂无邀请人员
</div>
<div class="empty-hint">
分享您的邀请码邀请好友注册吧
</div>
</div>
<!-- 分页 -->
<div v-if="totalInvites > 0 && totalPages > 1" class="pagination-section">
<div class="pagination-info">
{{ currentPage }}/{{ totalPages }}
</div>
<div class="pagination-buttons">
<ion-button
fill="outline"
size="small"
:disabled="currentPage === 1"
@click="prevPage"
>
上一页
</ion-button>
<ion-button
fill="outline"
size="small"
:disabled="currentPage === totalPages"
@click="nextPage"
>
下一页
</ion-button>
</div>
</div>
</section>
</div>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped>
.bind-card {
background: linear-gradient(135deg, #fff7f0 0%, #ffe8e8 100%);
border: 2px solid rgba(196, 30, 58, 0.1);
}
.bind-input {
--background: white;
--padding-start: 16px;
--padding-end: 16px;
--border-radius: 12px;
border: 2px solid #e8e8e8;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
letter-spacing: 2px;
text-align: center;
flex: 1;
}
.bind-input::part(native) {
font-weight: 600;
}
.bind-submit-btn {
--background: linear-gradient(135deg, #c41e3a 0%, #8b1a2e 100%);
--background-activated: #8b1a2e;
--border-radius: 12px;
--padding-start: 24px;
--padding-end: 24px;
font-weight: 700;
font-size: 16px;
height: 48px;
min-width: 90px;
text-transform: none;
}
.bind-submit-btn::part(native) {
font-weight: 700;
}
.card {
background: linear-gradient(180deg, #ffeef1, #ffffff 15%);
}
.invite-btn {
--background: white;
--color: #c41e3a;
--border-radius: 12px;
--box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-weight: 700;
font-size: 16px;
height: 50px;
text-transform: none;
}
.invite-btn::part(native) {
font-weight: 700;
}
.copy-btn {
--padding-start: 8px;
--padding-end: 8px;
margin: 0;
}
.copy-link-btn {
--background: #c41e3a;
--background-activated: #8b1a2e;
--border-radius: 8px;
--padding-start: 12px;
--padding-end: 12px;
height: 36px;
min-width: 36px;
}
.download-qr-btn {
--border-color: #c41e3a;
--color: #c41e3a;
--border-radius: 12px;
--border-width: 2px;
font-weight: 600;
font-size: 14px;
height: 44px;
text-transform: none;
}
.download-qr-btn::part(native) {
font-weight: 600;
}
.invite-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.invite-member-card {
background: white;
border-radius: 12px;
padding: 16px;
border: 1px solid #f0f0f0;
}
.member-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.member-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #c41e3a 0%, #8b1a2e 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
flex-shrink: 0;
}
.member-info {
flex: 1;
}
.member-name {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 4px;
}
.member-phone {
font-size: 13px;
color: #999;
}
.member-details {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #f5f5f5;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 13px;
color: #999;
}
.detail-value {
font-size: 14px;
color: #666;
font-weight: 500;
}
.detail-value.amount {
color: #c41e3a;
font-weight: 600;
font-size: 15px;
}
.empty-invite-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
}
.empty-icon {
font-size: 64px;
color: #ddd;
margin-bottom: 12px;
}
.empty-text {
font-size: 16px;
color: #999;
margin-bottom: 8px;
font-weight: 500;
}
.empty-hint {
font-size: 13px;
color: #bbb;
}
.pagination-section {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.pagination-info {
text-align: center;
font-size: 13px;
color: #666;
margin-bottom: 12px;
}
.pagination-buttons {
display: flex;
justify-content: center;
gap: 12px;
}
.pagination-buttons ion-button {
--border-color: #c41e3a;
--color: #c41e3a;
--border-radius: 8px;
font-size: 14px;
font-weight: 500;
min-width: 80px;
}
.pagination-buttons ion-button:not([disabled]):hover {
--background: rgba(196, 30, 58, 0.05);
}
.pagination-buttons ion-button[disabled] {
--border-color: #ddd;
--color: #999;
opacity: 0.5;
}
</style>

View File

@@ -4,22 +4,15 @@ import {
checkmarkCircleOutline,
copyOutline,
downloadOutline,
linkOutline,
peopleOutline,
qrCodeOutline,
shareOutline,
ticketOutline,
} from "ionicons/icons";
import { client, safeClient } from "@/api";
// 邀请信息
const inviteInfo = ref({
inviteCode: "ABCD1234",
inviteLink: "https://example.com/invite?code=ABCD1234",
downloadLink: "https://example.com/download",
qrCodeUrl: "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://example.com/invite?code=ABCD1234",
totalInvites: 28,
todayInvites: 3,
});
const userStore = useUserStore();
const { userProfile } = storeToRefs(userStore);
const { data } = safeClient(client.api.referrals.summary.get());
// 复制状态
const copyStatus = ref({
@@ -28,7 +21,11 @@ const copyStatus = ref({
download: false,
});
async function handleCopy(text: string, type: "link" | "code" | "download") {
async function handleCopy(text?: string, type: "link" | "code" | "download" = "link") {
if (!text) {
console.warn("没有可复制的文本");
return;
}
try {
await Clipboard.write({
string: text,
@@ -94,7 +91,7 @@ function handleDownloadQR() {
累计邀请
</div>
<div class="text-4xl font-bold mb-1">
{{ inviteInfo.totalInvites }}
{{ data?.totalCount }}
</div>
<div class="text-xs opacity-80">
@@ -105,7 +102,7 @@ function handleDownloadQR() {
今日邀请
</div>
<div class="text-4xl font-bold mb-1">
{{ inviteInfo.todayInvites }}
{{ data?.totalCount }}
</div>
<div class="text-xs opacity-80">
@@ -143,14 +140,14 @@ function handleDownloadQR() {
邀请码
</div>
<div class="text-xl font-bold text-[#c41e3a] tracking-wider">
{{ inviteInfo.inviteCode }}
{{ userProfile?.referralCode?.slice(0, 6) }}
</div>
</div>
</div>
<ion-button
fill="clear"
class="copy-btn"
@click="handleCopy(inviteInfo.inviteCode, 'code')"
@click="handleCopy(userProfile?.referralCode?.slice(0, 6), 'code')"
>
<ion-icon
slot="icon-only"
@@ -167,21 +164,21 @@ function handleDownloadQR() {
推广链接
</div>
</div>
<div class="mb-5 bg-gray-50 rounded-xl p-4">
<!-- <div class="mb-5 bg-gray-50 rounded-xl p-4">
<div class="flex items-center justify-between gap-3">
<div class="flex-1 overflow-hidden">
<div class="text-xs text-[#999] mb-1">
分享此链接邀请好友
</div>
<div class="text-sm text-[#333] truncate">
{{ inviteInfo.inviteLink }}
-
</div>
</div>
<ion-button
fill="solid"
size="small"
class="copy-link-btn"
@click="handleCopy(inviteInfo.inviteLink, 'link')"
@click="handleCopy('-', 'link')"
>
<ion-icon
slot="icon-only"
@@ -190,9 +187,9 @@ function handleDownloadQR() {
/>
</ion-button>
</div>
</div>
</div> -->
<div class="flex items-center gap-2 mb-3">
<!-- <div class="flex items-center gap-2 mb-3">
<img src="@/assets/images/icon.png" class="size-7">
<div class="text-lg font-bold text-[#1a1a1a]">
下载链接
@@ -221,7 +218,7 @@ function handleDownloadQR() {
/>
</ion-button>
</div>
</div>
</div> -->
</section>
<!-- 邀请二维码 -->
@@ -237,7 +234,7 @@ function handleDownloadQR() {
<div class="flex flex-col items-center">
<div class="bg-white p-4 rounded-2xl shadow-md mb-4 border-4 border-[#c41e3a]/20">
<img
:src="inviteInfo.qrCodeUrl"
src="https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=-"
alt="邀请二维码"
class="w-50 h-50"
>