feat(register): 完善经销商注册功能并优化邀请机制
- 将登录方法替换为注册方法,实现完整的用户注册流程 - 添加短信验证码发送失败的错误处理和提示 - 实现邀请推荐关系绑定功能,支持注册后自动建立推荐关系 - 优化URL参数解析,支持tenantId和inviter参数的灵活获取 - 添加倒计时清理逻辑,防止内存泄漏 - 更新表单验证规则,移除不必要的字段验证 - 在邀请链接中添加tenantId参数,确保未登录用户能正确识别租户 - 添加注册成功后的自动登录和推荐关系建立流程
This commit is contained in:
@@ -67,7 +67,7 @@
|
|||||||
size="large"
|
size="large"
|
||||||
:maxlength="6"
|
:maxlength="6"
|
||||||
allow-clear
|
allow-clear
|
||||||
@pressEnter="onLoginBySms"
|
@pressEnter="submit"
|
||||||
/>
|
/>
|
||||||
<a-button
|
<a-button
|
||||||
class="login-captcha"
|
class="login-captcha"
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
size="large"
|
size="large"
|
||||||
:maxlength="6"
|
:maxlength="6"
|
||||||
allow-clear
|
allow-clear
|
||||||
@pressEnter="onLoginBySms"
|
@pressEnter="submit"
|
||||||
/>
|
/>
|
||||||
<a-button
|
<a-button
|
||||||
class="login-captcha"
|
class="login-captcha"
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
size="large"
|
size="large"
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="onLoginBySms"
|
@click="submit"
|
||||||
>
|
>
|
||||||
{{ loading ? t('login.loading') : t('login.login') }}
|
{{ loading ? t('login.loading') : t('login.login') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, unref, watch, onMounted } from 'vue';
|
import { ref, reactive, unref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { getTenantId } from '@/utils/domain';
|
import { getTenantId } from '@/utils/domain';
|
||||||
@@ -203,22 +203,26 @@
|
|||||||
import { CheckCircleOutlined } from '@ant-design/icons-vue';
|
import { CheckCircleOutlined } from '@ant-design/icons-vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { goHomeRoute, cleanPageTabs } from '@/utils/page-tab-util';
|
import { goHomeRoute, cleanPageTabs } from '@/utils/page-tab-util';
|
||||||
import { loginBySms, getCaptcha } from '@/api/passport/login';
|
import {
|
||||||
import { TEMPLATE_ID, THEME_STORE_NAME } from '@/config/setting';
|
getCaptcha,
|
||||||
import { sendSmsCaptcha } from '@/api/passport/login';
|
loginBySms,
|
||||||
|
registerUser,
|
||||||
|
sendSmsCaptcha
|
||||||
|
} from '@/api/passport/login';
|
||||||
|
import { THEME_STORE_NAME } from '@/config/setting';
|
||||||
import useFormData from '@/utils/use-form-data';
|
import useFormData from '@/utils/use-form-data';
|
||||||
import { FormInstance } from 'ant-design-vue/es/form';
|
import { FormInstance } from 'ant-design-vue/es/form';
|
||||||
import { configWebsiteField } from '@/api/cms/cmsWebsiteField';
|
import { configWebsiteField } from '@/api/cms/cmsWebsiteField';
|
||||||
import { Config } from '@/api/cms/cmsWebsiteField/model';
|
import { Config } from '@/api/cms/cmsWebsiteField/model';
|
||||||
import { phoneReg } from 'ele-admin-pro';
|
import { phoneReg } from 'ele-admin-pro';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
|
|
||||||
import { addAdminUser } from '@/api/system/user';
|
|
||||||
import { User } from '@/api/system/user/model';
|
import { User } from '@/api/system/user/model';
|
||||||
import { listRoles } from '@/api/system/role';
|
import { addShopDealerReferee } from '@/api/shop/shopDealerReferee';
|
||||||
|
import { getToken } from '@/utils/token-util';
|
||||||
|
|
||||||
const useForm = Form.useForm;
|
const useForm = Form.useForm;
|
||||||
const { currentRoute } = useRouter();
|
const router = useRouter();
|
||||||
|
const { currentRoute } = router;
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
|
||||||
@@ -237,7 +241,10 @@
|
|||||||
// 配置信息
|
// 配置信息
|
||||||
const { form } = useFormData<User>({
|
const { form } = useFormData<User>({
|
||||||
phone: '',
|
phone: '',
|
||||||
isAdmin: true
|
code: '',
|
||||||
|
remember: false, // 这里作为“同意协议”的勾选状态使用
|
||||||
|
isAdmin: true,
|
||||||
|
dealerId: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// 验证码 base64 数据
|
// 验证码 base64 数据
|
||||||
@@ -262,22 +269,8 @@
|
|||||||
// 表格选中数据
|
// 表格选中数据
|
||||||
const formRef = ref<FormInstance | null>(null);
|
const formRef = ref<FormInstance | null>(null);
|
||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则(只保留本页面实际填写的字段)
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
companyName: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入店铺名称',
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
category: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择行业分类',
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
phone: [
|
phone: [
|
||||||
{
|
{
|
||||||
pattern: phoneReg,
|
pattern: phoneReg,
|
||||||
@@ -287,33 +280,12 @@
|
|||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
// address: [
|
|
||||||
// {
|
|
||||||
// required: true,
|
|
||||||
// message: '请选择店铺位置',
|
|
||||||
// type: 'string'
|
|
||||||
// }
|
|
||||||
// ],
|
|
||||||
password: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t('login.password'),
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
code: [
|
code: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t('login.code'),
|
message: t('login.code'),
|
||||||
type: 'string'
|
type: 'string'
|
||||||
}
|
}
|
||||||
],
|
|
||||||
smsCode: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t('login.code'),
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -339,20 +311,26 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
codeLoading.value = true;
|
codeLoading.value = true;
|
||||||
sendSmsCaptcha({ phone: form.phone }).then(() => {
|
sendSmsCaptcha({ phone: form.phone })
|
||||||
message.success('短信验证码发送成功, 请注意查收!');
|
.then(() => {
|
||||||
visible.value = false;
|
message.success('短信验证码发送成功, 请注意查收!');
|
||||||
codeLoading.value = false;
|
visible.value = false;
|
||||||
countdownTime.value = 30;
|
countdownTime.value = 30;
|
||||||
// 开始对按钮进行倒计时
|
// 开始对按钮进行倒计时
|
||||||
countdownTimer = window.setInterval(() => {
|
countdownTimer = window.setInterval(() => {
|
||||||
if (countdownTime.value <= 1) {
|
if (countdownTime.value <= 1) {
|
||||||
countdownTimer && clearInterval(countdownTimer);
|
countdownTimer && clearInterval(countdownTimer);
|
||||||
countdownTimer = null;
|
countdownTimer = null;
|
||||||
}
|
}
|
||||||
countdownTime.value--;
|
countdownTime.value--;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
})
|
||||||
|
.catch((e: any) => {
|
||||||
|
message.error(e?.message || '短信验证码发送失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
codeLoading.value = false;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { resetFields } = useForm(form, rules);
|
const { resetFields } = useForm(form, rules);
|
||||||
@@ -363,29 +341,30 @@
|
|||||||
localStorage.removeItem(THEME_STORE_NAME);
|
localStorage.removeItem(THEME_STORE_NAME);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLoginBySms = () => {
|
// 建立邀请推荐关系(需要登录态;失败不影响主流程)
|
||||||
if (!formRef.value) {
|
let bindInviterOnce = false;
|
||||||
|
const tryBindInviteRelation = async () => {
|
||||||
|
if (bindInviterOnce) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formRef.value
|
if (!inviterId.value) {
|
||||||
.validate()
|
return;
|
||||||
.then(() => {
|
}
|
||||||
loading.value = true;
|
if (!getToken()) {
|
||||||
form.code = form.smsCode?.toLowerCase();
|
return;
|
||||||
loginBySms(form)
|
}
|
||||||
.then((msg) => {
|
const tenantIdStr = localStorage.getItem('TenantId');
|
||||||
message.success(msg);
|
const userIdStr = localStorage.getItem('UserId');
|
||||||
loading.value = false;
|
if (!userIdStr) {
|
||||||
resetFields();
|
return;
|
||||||
cleanPageTabs();
|
}
|
||||||
goHome();
|
bindInviterOnce = true;
|
||||||
})
|
await addShopDealerReferee({
|
||||||
.catch((e: Error) => {
|
dealerId: inviterId.value,
|
||||||
message.error(e.message);
|
userId: Number(userIdStr),
|
||||||
loading.value = false;
|
level: 1,
|
||||||
});
|
tenantId: tenantIdStr ? Number(tenantIdStr) : form.tenantId
|
||||||
})
|
});
|
||||||
.catch(() => {});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* 保存编辑 */
|
/* 保存编辑 */
|
||||||
@@ -394,52 +373,53 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
formRef.value
|
try {
|
||||||
.validate()
|
await formRef.value.validate();
|
||||||
.then(() => {
|
} catch {
|
||||||
loading.value = true;
|
return;
|
||||||
// addShopDealerUser({
|
}
|
||||||
// ...form,
|
|
||||||
// dealerId: Number(dealerId.value),
|
|
||||||
// }).then((data) => {
|
|
||||||
// console.log(data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// createCmsWebSite(form)
|
loading.value = true;
|
||||||
// .then(async (msg) => {
|
try {
|
||||||
// // 如果有邀请人,注册成功后建立推荐关系
|
// 通过短信验证码完成注册;inviter 作为推荐人ID传给后端
|
||||||
// if (inviterId.value) {
|
const msg = await registerUser({
|
||||||
// try {
|
tenantId: form.tenantId,
|
||||||
// const userId = localStorage.getItem('UserId');
|
phone: form.phone,
|
||||||
// if (userId) {
|
username: form.phone,
|
||||||
// await bindUserReferee({
|
code: form.code?.trim(),
|
||||||
// dealerId: inviterId.value,
|
// 后端若用 Java boolean 命名为 `isAdmin`,序列化/反序列化可能会被映射成 `admin`;
|
||||||
// userId: Number(userId),
|
// 这里双写以兼容不同字段名,避免后端拿到 null 导致 NPE。
|
||||||
// level: 1
|
isAdmin: true,
|
||||||
// });
|
...({ admin: true } as any),
|
||||||
// message.success('注册成功,已建立推荐关系');
|
dealerId: inviterId.value
|
||||||
// }
|
});
|
||||||
// } catch (e) {
|
|
||||||
// console.error('建立推荐关系失败:', e);
|
// 注册成功后自动登录,再建立绑定关系(避免未登录时后端取 role 为空)
|
||||||
// // 不影响注册流程,只是推荐关系建立失败
|
await loginBySms({
|
||||||
// }
|
tenantId: form.tenantId,
|
||||||
// }
|
phone: form.phone,
|
||||||
//
|
code: form.code?.trim(),
|
||||||
// setTimeout(() => {
|
remember: !!form.remember
|
||||||
// // 登录成功
|
});
|
||||||
// message.success(msg);
|
|
||||||
// loading.value = false;
|
// 建立绑定关系(失败不影响主流程)
|
||||||
// resetFields();
|
try {
|
||||||
// cleanPageTabs();
|
await tryBindInviteRelation();
|
||||||
// goHome();
|
} catch (e: any) {
|
||||||
// }, 2000)
|
message.warning(
|
||||||
// })
|
`注册成功,但建立推荐关系失败:${e?.message || '未知错误'}`
|
||||||
// .catch(() => {
|
);
|
||||||
// message.error('该手机号码已经被注册');
|
}
|
||||||
// loading.value = false;
|
|
||||||
// });
|
message.success(msg);
|
||||||
})
|
resetFields();
|
||||||
.catch(() => {});
|
cleanPageTabs();
|
||||||
|
goHome();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '注册失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* 获取图形验证码 */
|
/* 获取图形验证码 */
|
||||||
@@ -469,13 +449,51 @@
|
|||||||
|
|
||||||
// 检查URL参数中的邀请人ID
|
// 检查URL参数中的邀请人ID
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
// 优先从链接参数中拿 tenantId(新用户未登录时,用于后端识别租户/初始化角色)
|
||||||
const inviterParam = urlParams.get('inviter');
|
const tenantIdParam = (unref(currentRoute).query.tenantId ??
|
||||||
if (inviterParam) {
|
new URLSearchParams(window.location.search).get('tenantId')) as
|
||||||
inviterId.value = Number(inviterParam);
|
| string
|
||||||
|
| undefined;
|
||||||
|
if (tenantIdParam != null && tenantIdParam !== '') {
|
||||||
|
const tid = Number.parseInt(String(tenantIdParam).trim(), 10);
|
||||||
|
if (!Number.isNaN(tid) && tid > 0) {
|
||||||
|
form.tenantId = tid;
|
||||||
|
// 让 request 拦截器也能带上 TenantId
|
||||||
|
localStorage.setItem('TenantId', String(tid));
|
||||||
|
showTenantId.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inviterParam = (unref(currentRoute).query.inviter ??
|
||||||
|
new URLSearchParams(window.location.search).get('inviter')) as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
if (inviterParam != null && inviterParam !== '') {
|
||||||
|
// 容错:有些场景会把链接后面带上逗号/空格等(例如复制文本),这里提取首段数字
|
||||||
|
const raw = String(inviterParam).trim();
|
||||||
|
const id = Number.parseInt(raw.match(/\d+/)?.[0] || '', 10);
|
||||||
|
if (!Number.isNaN(id) && id > 0) {
|
||||||
|
inviterId.value = id;
|
||||||
|
form.dealerId = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录用户通过邀请链接访问时,补绑一次推荐关系
|
||||||
|
tryBindInviteRelation();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (countdownTimer) {
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
countdownTimer = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 地图选择回调(本页面不使用地图能力,避免模板引用报错)
|
||||||
|
const onDone = () => {
|
||||||
|
showMap.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
currentRoute,
|
currentRoute,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -169,10 +169,21 @@
|
|||||||
return props.inviterId || Number(localStorage.getItem('UserId'));
|
return props.inviterId || Number(localStorage.getItem('UserId'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 邀请链接需要带上 tenantId,避免未登录用户打开链接时后端无法识别租户,导致角色/权限初始化失败
|
||||||
|
const tenantId = computed(() => {
|
||||||
|
const tid = localStorage.getItem('TenantId');
|
||||||
|
return tid ? Number(tid) : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
// 生成邀请链接
|
// 生成邀请链接
|
||||||
const invitationLink = computed(() => {
|
const invitationLink = computed(() => {
|
||||||
const baseUrl = window.location.origin;
|
const baseUrl = window.location.origin;
|
||||||
return `${baseUrl}/dealer/register?inviter=${inviterId.value}`;
|
const params = new URLSearchParams();
|
||||||
|
params.set('inviter', String(inviterId.value));
|
||||||
|
if (tenantId.value) {
|
||||||
|
params.set('tenantId', String(tenantId.value));
|
||||||
|
}
|
||||||
|
return `${baseUrl}/dealer/register?${params.toString()}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 复制链接
|
// 复制链接
|
||||||
|
|||||||
Reference in New Issue
Block a user