feat(shop): 添加邀请注册功能并优化注册流程

- 新增邀请注册弹窗组件,用于生成邀请链接和二维码
- 在注册页面添加邀请信息显示,引导用户通过邀请链接注册
- 实现注册成功后自动建立推荐关系的功能
- 优化注册表单,支持通过邀请链接直接进入注册页面
This commit is contained in:
2025-09-05 15:01:21 +08:00
parent c6e6bb02d3
commit cf74d2670c
3 changed files with 209 additions and 4 deletions

View File

@@ -35,6 +35,14 @@
>
</div>
<template v-if="loginType === 'account'">
<!-- 邀请信息显示 -->
<div v-if="inviterId" style="margin-bottom: 16px; padding: 12px; background: #f6ffed; border: 1px solid #b7eb8f; border-radius: 4px;">
<div style="color: #52c41a; font-size: 14px;">
<check-circle-outlined style="margin-right: 4px;" />
您正在通过邀请链接注册注册成功后将自动建立推荐关系
</div>
</div>
<a-form-item name="phone">
<a-input
allow-clear
@@ -227,12 +235,12 @@
</template>
<script lang="ts" setup>
import {ref, reactive, unref, watch} from 'vue';
import {ref, reactive, unref, watch, onMounted} from 'vue';
import {useI18n} from 'vue-i18n';
import {useRouter} from 'vue-router';
import {getTenantId} from '@/utils/domain';
import {Form, message} from 'ant-design-vue';
import { AimOutlined } from '@ant-design/icons-vue';
import { AimOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons-vue';
import { storeToRefs } from 'pinia';
import {goHomeRoute, cleanPageTabs} from '@/utils/page-tab-util';
import {loginBySms, getCaptcha} from '@/api/passport/login';
@@ -248,6 +256,7 @@ import {push} from "@/utils/common";
import {useThemeStore} from "@/store/modules/theme";
import {CmsWebsite} from "@/api/cms/cmsWebsite/model";
import {createCmsWebSite} from "@/api/layout";
import { bindUserReferee } from '@/api/user/referee';
const useForm = Form.useForm;
const {currentRoute} = useRouter();
@@ -382,6 +391,8 @@ const countdownTime = ref(0);
let countdownTimer: number | null = null;
// 是否显示地图选择弹窗
const showMap = ref(false);
// 邀请人ID
const inviterId = ref<number>();
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
@@ -548,8 +559,28 @@ const submit = () => {
.validate()
.then(() => {
loading.value = true;
// 普通注册流程
createCmsWebSite(form)
.then((msg) => {
.then(async (msg) => {
// 如果有邀请人,注册成功后建立推荐关系
if (inviterId.value) {
try {
const userId = localStorage.getItem('UserId');
if (userId) {
await bindUserReferee({
dealerId: inviterId.value,
userId: Number(userId),
level: 1
});
message.success('注册成功,已建立推荐关系');
}
} catch (e) {
console.error('建立推荐关系失败:', e);
// 不影响注册流程,只是推荐关系建立失败
}
}
setTimeout(() => {
// 登录成功
message.success(msg);
@@ -593,6 +624,15 @@ const changeCaptcha = () => {
// 首次加载
changeCaptcha();
// 检查URL参数中的邀请人ID
onMounted(() => {
const urlParams = new URLSearchParams(window.location.search);
const inviterParam = urlParams.get('inviter');
if (inviterParam) {
inviterId.value = Number(inviterParam);
}
});
watch(
currentRoute,
() => {

View File

@@ -0,0 +1,146 @@
<template>
<a-modal
:width="500"
:visible="visible"
:footer="null"
title="邀请注册"
@update:visible="updateVisible"
>
<div style="text-align: center">
<div style="margin-bottom: 20px">
<a-typography-title :level="4">邀请新用户注册</a-typography-title>
<a-typography-text type="secondary">
分享以下链接或二维码邀请用户注册并自动建立推荐关系
</a-typography-text>
</div>
<!-- 邀请链接 -->
<div style="margin-bottom: 20px">
<a-input
:value="invitationLink"
readonly
style="margin-bottom: 8px"
>
<template #addonAfter>
<a-button type="link" size="small" @click="copyLink">
复制链接
</a-button>
</template>
</a-input>
</div>
<!-- 二维码 -->
<div style="margin-bottom: 20px">
<div style="display: inline-block; padding: 10px; background: white; border-radius: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1)">
<QrCode
:value="invitationLink"
:size="200"
:margin="10"
/>
</div>
</div>
<!-- 使用说明 -->
<div style="text-align: left; background: #f5f5f5; padding: 12px; border-radius: 4px; margin-bottom: 20px">
<div style="font-weight: 500; margin-bottom: 8px">使用说明</div>
<div style="font-size: 12px; color: #666; line-height: 1.5">
1. 复制邀请链接发送给用户或让用户扫描二维码<br>
2. 用户点击链接进入注册页面<br>
3. 用户完成注册后系统自动建立推荐关系<br>
4. 您可以在"推荐关系管理"中查看邀请结果
</div>
</div>
<!-- 操作按钮 -->
<div>
<a-space>
<a-button @click="downloadQRCode">下载二维码</a-button>
<a-button type="primary" @click="copyLink">复制链接</a-button>
<a-button @click="goToRefereeManage">查看推荐关系</a-button>
</a-space>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import { useRouter } from 'vue-router';
import QrCode from '@/components/QrCode/index.vue';
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
visible: boolean;
inviterId?: number; // 邀请人ID当前登录用户ID
}>();
const router = useRouter();
// 生成邀请链接
const invitationLink = computed(() => {
const baseUrl = window.location.origin;
const inviterId = props.inviterId || localStorage.getItem('UserId');
return `${baseUrl}/register?inviter=${inviterId}`;
});
// 复制链接
const copyLink = async () => {
try {
await navigator.clipboard.writeText(invitationLink.value);
message.success('邀请链接已复制到剪贴板');
} catch (e) {
// 降级方案
const textArea = document.createElement('textarea');
textArea.value = invitationLink.value;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
message.success('邀请链接已复制到剪贴板');
}
};
// 下载二维码
const downloadQRCode = () => {
try {
// 查找二维码canvas元素
const canvas = document.querySelector('.ant-modal-body canvas') as HTMLCanvasElement;
if (canvas) {
const link = document.createElement('a');
link.download = `邀请注册二维码.png`;
link.href = canvas.toDataURL();
link.click();
message.success('二维码已下载');
} else {
message.error('下载失败,请稍后重试');
}
} catch (e) {
message.error('下载失败');
}
};
// 跳转到推荐关系管理
const goToRefereeManage = () => {
router.push('/shop/user-referee');
updateVisible(false);
};
// 更新visible
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>
<style lang="less" scoped>
:deep(.ant-typography-title) {
margin-bottom: 8px !important;
}
:deep(.ant-input-group-addon) {
padding: 0;
}
</style>

View File

@@ -16,7 +16,7 @@
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<a-button type="primary" class="ele-btn-icon" @click="openInvitation">
<template #icon>
<plus-outlined/>
</template>
@@ -105,6 +105,8 @@
<user-import v-model:visible="showImport" @done="reload"/>
<!-- 用户详情 -->
<user-info v-model:visible="showInfo" :data="current" @done="reload"/>
<!-- 邀请注册弹窗 -->
<invitation-modal v-model:visible="showInvitation" :inviter-id="currentUserId"/>
</a-page-header>
</template>
@@ -128,6 +130,7 @@ import {messageLoading, formatNumber} from 'ele-admin-pro/es';
import UserEdit from './components/user-edit.vue';
import UserImport from './components/user-import.vue';
import UserInfo from './components/user-info.vue';
import InvitationModal from './components/invitation-modal.vue';
import {toDateString} from 'ele-admin-pro';
import {
pageUsers,
@@ -165,8 +168,12 @@ const showEdit = ref(false);
const showInfo = ref(false);
// 是否显示用户导入弹窗
const showImport = ref(false);
// 是否显示邀请注册弹窗
const showInvitation = ref(false);
const userType = ref<number>();
const searchText = ref('');
// 当前用户ID
const currentUserId = ref<number>();
// 加载角色
const roles = ref<any[]>([]);
@@ -410,6 +417,18 @@ const query = async () => {
}
}
/* 打开邀请注册弹窗 */
const openInvitation = () => {
// 获取当前用户ID
const userId = localStorage.getItem('UserId');
if (userId) {
currentUserId.value = Number(userId);
showInvitation.value = true;
} else {
message.error('获取用户信息失败');
}
};
watch(
() => router.currentRoute.value.query,
() => {