update user invite style
This commit is contained in:
@@ -32,7 +32,7 @@ export type InviteRegisterData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type InviteRegisterRequest = {
|
export type InviteRegisterRequest = {
|
||||||
captchaId: string;
|
detailId: string;
|
||||||
captcha: string;
|
captcha: string;
|
||||||
data: InviteRegisterData;
|
data: InviteRegisterData;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="register-page">
|
<div class="register-page">
|
||||||
<section v-if="isLoading" class="register-panel"><t-loading text="正在校验邀请链接" /></section>
|
<section v-if="isLoading" class="register-panel"><t-loading text="正在校验邀请码" /></section>
|
||||||
<section v-else-if="!isValid" class="register-panel invalid">
|
<section v-else-if="!isValid" class="register-panel invalid">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="logo"><span>糕</span></div>
|
<logo />
|
||||||
<h1>糕店客户管理</h1>
|
<h1>糕店客户管理</h1>
|
||||||
</div>
|
</div>
|
||||||
<h2>邀请链接已失效</h2>
|
<h2>邀请码已失效</h2>
|
||||||
<p v-text="invalidMessage" />
|
<p class="tips gray" v-text="invalidMessage" />
|
||||||
</section>
|
</section>
|
||||||
<section v-else class="register-panel">
|
<section v-else class="register-panel">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="logo"><span>糕</span></div>
|
<logo />
|
||||||
<h1>加入糕店客户管理</h1>
|
<h1>加入糕店客户管理系统</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="expire" v-text="expireText" />
|
<p class="tips gray" v-text="expireText" />
|
||||||
<t-form class="form" label-align="left">
|
<t-form class="form" label-align="left" label-width="6rem">
|
||||||
<t-form-item label="手机号">
|
<t-form-item label="手机号">
|
||||||
<t-input v-model.trim="formModel.phoneNo" type="tel" inputmode="tel" borderless placeholder="请输入手机号" />
|
<t-input v-model.trim="formModel.phoneNo" type="tel" inputmode="tel" borderless placeholder="请输入手机号" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
@@ -28,23 +28,38 @@
|
|||||||
<t-form-item label="确认密码">
|
<t-form-item label="确认密码">
|
||||||
<t-input v-model="formModel.confirmPassword" type="password" borderless placeholder="请再次输入登录密码" />
|
<t-input v-model="formModel.confirmPassword" type="password" borderless placeholder="请再次输入登录密码" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="验证码">
|
<t-form-item label="图形验证码">
|
||||||
<div class="captcha-row">
|
|
||||||
<t-input v-model.trim="formModel.captcha" borderless placeholder="请输入验证码" />
|
<t-input v-model.trim="formModel.captcha" borderless placeholder="请输入验证码" />
|
||||||
<captcha ref="captchaRef" class="captcha" @error="handleCaptchaError" />
|
|
||||||
</div>
|
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
|
<div class="captcha-wrapper">
|
||||||
|
<captcha ref="captchaRef" class="captcha" :width="120" :height="40" @error="handleCaptchaError" />
|
||||||
|
</div>
|
||||||
|
<t-form-item label="短信验证码">
|
||||||
|
<t-input v-model.trim="formModel.smsCode" borderless placeholder="请输入短信验证码" />
|
||||||
|
</t-form-item>
|
||||||
|
<div class="send-wrapper">
|
||||||
|
<t-button
|
||||||
|
class="send-sms"
|
||||||
|
type="button"
|
||||||
|
size="extra-small"
|
||||||
|
theme="primary"
|
||||||
|
:content="sendSmsText"
|
||||||
|
:disabled="!canSendSms"
|
||||||
|
@click="sendSms"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
<t-button class="submit" theme="primary" size="medium" :loading="isSubmitting" :disabled="isExpired" @click="register">注册并登录</t-button>
|
<t-button class="submit" theme="primary" size="medium" :loading="isSubmitting" :disabled="isExpired || isSubmitting" @click="register">注册并登录</t-button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { Captcha, useUserStore, type LoginResponse, Time } from "timi-web";
|
import { Captcha, NotifyAPI, useUserStore, type LoginResponse, Time } from "timi-web";
|
||||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
|
import Logo from "@/pages/user/component/Logo.vue";
|
||||||
|
|
||||||
type CaptchaExpose = { captchaId: string; update: () => Promise<void> };
|
type CaptchaExpose = { captchaId: string; update: () => Promise<void> };
|
||||||
|
|
||||||
@@ -57,17 +72,43 @@ const code = String(route.query.code || "");
|
|||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const isValid = ref(false);
|
const isValid = ref(false);
|
||||||
const expireAt = ref(0);
|
const expireAt = ref(0);
|
||||||
const invalidMessage = ref("邀请链接无效或已过期,请联系邀请人重新生成。");
|
const invalidMessage = ref("邀请码无效或已过期,请联系邀请人重新生成。");
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
const captchaRef = ref<CaptchaExpose | null>(null);
|
const captchaRef = ref<CaptchaExpose | null>(null);
|
||||||
const formModel = reactive({ phoneNo: "", name: "", password: "", confirmPassword: "", captcha: "" });
|
const formModel = reactive({
|
||||||
|
phoneNo: "",
|
||||||
|
name: "",
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
captcha: "",
|
||||||
|
smsCode: "",
|
||||||
|
notifyDetailId: ""
|
||||||
|
});
|
||||||
const currentTime = ref(Date.now());
|
const currentTime = ref(Date.now());
|
||||||
const isExpired = computed(() => !expireAt.value || currentTime.value >= expireAt.value);
|
const isExpired = computed(() => !expireAt.value || currentTime.value >= expireAt.value);
|
||||||
const expireText = computed(() => isExpired.value ? "邀请链接已失效" : `有效期至 ${Time.toDateTime(expireAt.value)}`);
|
const expireText = computed(() => isExpired.value ? "邀请码已失效" : `有效期至 ${Time.toDateTime(expireAt.value)}`);
|
||||||
|
const sendSmsCountdown = ref(0);
|
||||||
|
let sendSmsTimer: number | undefined;
|
||||||
|
const isSendingSms = computed(() => 0 < sendSmsCountdown.value);
|
||||||
|
const sendSmsText = computed(() => isSendingSms.value ? `${sendSmsCountdown.value} 秒后重新发送` : "发送验证码");
|
||||||
|
const canSendSms = computed(() => (
|
||||||
|
!isExpired.value
|
||||||
|
&& !isSendingSms.value
|
||||||
|
&& /^1\d{10}$/.test(formModel.phoneNo)
|
||||||
|
&& 4 === formModel.captcha.length
|
||||||
|
));
|
||||||
|
|
||||||
|
watch(() => formModel.phoneNo, () => {
|
||||||
|
if (!formModel.notifyDetailId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formModel.notifyDetailId = "";
|
||||||
|
formModel.smsCode = "";
|
||||||
|
});
|
||||||
|
|
||||||
async function validateInvite(): Promise<void> {
|
async function validateInvite(): Promise<void> {
|
||||||
if (!/^\w{10}$/.test(code)) {
|
if (!/^\w{10}$/.test(code)) {
|
||||||
invalidMessage.value = "邀请链接格式不正确。";
|
invalidMessage.value = "邀请码格式不正确。";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await ManagedUserAPI.validateInvite(code);
|
const result = await ManagedUserAPI.validateInvite(code);
|
||||||
@@ -92,39 +133,89 @@ function validateForm(): boolean {
|
|||||||
Toast.warning("两次输入的密码不一致");
|
Toast.warning("两次输入的密码不一致");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!captchaRef.value?.captchaId || !formModel.captcha) {
|
if (!formModel.notifyDetailId || !/^\d{6}$/.test(formModel.smsCode)) {
|
||||||
Toast.warning("请输入图形验证码");
|
Toast.warning("请先获取并输入短信验证码");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCaptchaId(): string {
|
||||||
|
return captchaRef.value?.captchaId || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSmsTimer(): void {
|
||||||
|
if (sendSmsTimer === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.clearInterval(sendSmsTimer);
|
||||||
|
sendSmsTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSmsTimer(): void {
|
||||||
|
clearSmsTimer();
|
||||||
|
sendSmsCountdown.value = 60;
|
||||||
|
sendSmsTimer = window.setInterval(() => {
|
||||||
|
sendSmsCountdown.value -= 1;
|
||||||
|
if (!(0 < sendSmsCountdown.value)) {
|
||||||
|
clearSmsTimer();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendSms(): Promise<void> {
|
||||||
|
if (!canSendSms.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const captchaId = getCaptchaId();
|
||||||
|
if (!captchaId) {
|
||||||
|
Toast.warning("图形验证码尚未加载,请稍后重试");
|
||||||
|
await captchaRef.value?.update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
formModel.notifyDetailId = await NotifyAPI.smsSend({
|
||||||
|
captchaId,
|
||||||
|
captcha: formModel.captcha,
|
||||||
|
data: {
|
||||||
|
detailList: [{ sendTo: formModel.phoneNo }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
startSmsTimer();
|
||||||
|
Toast.success(`验证码已发送,请检查 ${formModel.phoneNo} 手机短信(易保网络)`);
|
||||||
|
} catch {
|
||||||
|
formModel.notifyDetailId = "";
|
||||||
|
} finally {
|
||||||
|
formModel.captcha = "";
|
||||||
|
await captchaRef.value?.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function register(): Promise<void> {
|
async function register(): Promise<void> {
|
||||||
if (isSubmitting.value || isExpired.value || !validateForm()) {
|
if (isSubmitting.value || isExpired.value || !validateForm()) {
|
||||||
if (isExpired.value) {
|
if (isExpired.value) {
|
||||||
Toast.warning("邀请链接已失效,请联系邀请人重新生成");
|
Toast.warning("邀请码已失效,请联系邀请人重新生成");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await ManagedUserAPI.registerByInvite({
|
const result = await ManagedUserAPI.registerByInvite({
|
||||||
captchaId: captchaRef.value?.captchaId || "",
|
|
||||||
captcha: formModel.captcha,
|
|
||||||
data: {
|
data: {
|
||||||
code,
|
code,
|
||||||
phoneNo: formModel.phoneNo,
|
phoneNo: formModel.phoneNo,
|
||||||
name: formModel.name,
|
name: formModel.name,
|
||||||
password: formModel.password
|
password: formModel.password
|
||||||
}
|
},
|
||||||
|
detailId: formModel.notifyDetailId,
|
||||||
|
captcha: formModel.smsCode
|
||||||
});
|
});
|
||||||
await userStore.updateLoginUser(result as LoginResponse);
|
await userStore.updateLoginUser(result as LoginResponse);
|
||||||
await currentStoreStore.loadAfterLogin();
|
await currentStoreStore.loadAfterLogin();
|
||||||
Toast.success("注册成功");
|
Toast.success("注册成功");
|
||||||
await router.replace("/management");
|
await router.replace("/management");
|
||||||
} catch {
|
} catch {
|
||||||
formModel.captcha = "";
|
formModel.smsCode = "";
|
||||||
await captchaRef.value?.update();
|
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
}
|
}
|
||||||
@@ -143,7 +234,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
await validateInvite();
|
await validateInvite();
|
||||||
} catch {
|
} catch {
|
||||||
invalidMessage.value = "邀请链接校验失败,请稍后重试。";
|
invalidMessage.value = "邀请码校验失败,请稍后重试。";
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -151,6 +242,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.clearInterval(expireTimer);
|
window.clearInterval(expireTimer);
|
||||||
|
clearSmsTimer();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -175,6 +267,14 @@ onUnmounted(() => {
|
|||||||
border-radius: 1.4rem;
|
border-radius: 1.4rem;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.invalid {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -187,62 +287,30 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.tips {
|
||||||
width: 5rem;
|
|
||||||
height: 5rem;
|
|
||||||
color: #FFF;
|
|
||||||
display: grid;
|
|
||||||
background: #0f766e;
|
|
||||||
box-shadow: 0 .75rem 1.5rem rgba(15, 118, 110, .22);
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.expire,
|
|
||||||
p,
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expire,
|
|
||||||
p {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
}
|
text-align: center;
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: .25rem 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-row {
|
.send-wrapper,
|
||||||
gap: .75rem;
|
.captcha-wrapper {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
margin-top: .5rem;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
.t-input {
|
.captcha {
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.tui-captcha) {
|
|
||||||
flex: 0 0 7.5rem;
|
|
||||||
border: 1px solid var(--td-border-level-1-color);
|
border: 1px solid var(--td-border-level-1-color);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-sms {
|
||||||
|
flex: 0 0 7.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.submit {
|
.submit {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<section class="login-panel">
|
<section class="login-panel">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="logo"><span>糕</span></div>
|
<logo />
|
||||||
<h1>糕店客户管理</h1>
|
<h1>糕店客户管理</h1>
|
||||||
</div>
|
</div>
|
||||||
<t-segmented class="login-type" v-model="loginType" :options="loginTypeOptions" />
|
<t-segmented class="login-type" v-model="loginType" :options="loginTypeOptions" />
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PasswordLogin from "@/pages/user/component/PasswordLogin.vue";
|
import PasswordLogin from "@/pages/user/component/PasswordLogin.vue";
|
||||||
import SmsLogin from "@/pages/user/component/SmsLogin.vue";
|
import SmsLogin from "@/pages/user/component/SmsLogin.vue";
|
||||||
|
import Logo from "@/pages/user/component/Logo.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "LoginIndex"
|
name: "LoginIndex"
|
||||||
@@ -74,22 +75,6 @@ const loginTypeOptions = [
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 5rem;
|
|
||||||
height: 5rem;
|
|
||||||
color: #FFF;
|
|
||||||
display: grid;
|
|
||||||
background: #0f766e;
|
|
||||||
box-shadow: 0 .75rem 1.5rem rgba(15, 118, 110, .22);
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
|
|||||||
@@ -5,8 +5,20 @@
|
|||||||
<div v-else class="empty" v-text="message" />
|
<div v-else class="empty" v-text="message" />
|
||||||
</section>
|
</section>
|
||||||
<div v-if="posterImage" class="actions">
|
<div v-if="posterImage" class="actions">
|
||||||
<t-button class="bg-white" variant="outline" theme="primary" block @click="copyLink">复制链接</t-button>
|
<t-button
|
||||||
<t-button theme="primary" block :loading="isDownloading" @click="savePoster">保存海报</t-button>
|
class="button bg-white"
|
||||||
|
variant="outline"
|
||||||
|
theme="primary"
|
||||||
|
block
|
||||||
|
@click="copyLink"
|
||||||
|
>复制链接</t-button>
|
||||||
|
<t-button
|
||||||
|
class="button"
|
||||||
|
theme="primary"
|
||||||
|
block
|
||||||
|
:loading="isDownloading"
|
||||||
|
@click="savePoster"
|
||||||
|
>保存海报</t-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,13 +117,11 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
--td-button-border-radius: 1rem;
|
||||||
|
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
left: 1rem;
|
width: 100%;
|
||||||
right: 1rem;
|
display: flex;
|
||||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
|
||||||
display: grid;
|
|
||||||
position: fixed;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,10 +37,10 @@
|
|||||||
<t-cell title="修改密码" arrow hover @click="toPassword" />
|
<t-cell title="修改密码" arrow hover @click="toPassword" />
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-button
|
<t-button
|
||||||
class="logout bg-white"
|
class="red bg-white cell-button"
|
||||||
theme="danger"
|
variant="text"
|
||||||
|
block
|
||||||
@click="confirmLogout"
|
@click="confirmLogout"
|
||||||
variant="outline"
|
|
||||||
>退出登录</t-button>
|
>退出登录</t-button>
|
||||||
<input
|
<input
|
||||||
ref="avatarInputRef"
|
ref="avatarInputRef"
|
||||||
@@ -252,11 +252,6 @@ onUnmounted(() => {
|
|||||||
color: var(--td-text-color-primary);
|
color: var(--td-text-color-primary);
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout {
|
|
||||||
margin: 1rem auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="logo">
|
||||||
|
<span>糕</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped lang="less">
|
||||||
|
.logo {
|
||||||
|
color: #FFF;
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
display: grid;
|
||||||
|
background: #0f766e;
|
||||||
|
box-shadow: 0 .75rem 1.5rem rgba(15, 118, 110, .22);
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Message } from "@/utils/message";
|
||||||
import { Captcha, NotifyAPI, UserAPI, useUserStore } from "timi-web";
|
import { Captcha, NotifyAPI, UserAPI, useUserStore } from "timi-web";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ async function sendSms(): Promise<void> {
|
|||||||
|
|
||||||
const captchaId = getCaptchaId();
|
const captchaId = getCaptchaId();
|
||||||
if (!captchaId) {
|
if (!captchaId) {
|
||||||
Toast.warning("图形验证码尚未加载,请稍后重试");
|
Message.warning("图形验证码尚未加载,请稍后重试");
|
||||||
await captchaRef.value?.update();
|
await captchaRef.value?.update();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ async function sendSms(): Promise<void> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
startSmsTimer();
|
startSmsTimer();
|
||||||
Toast.success(`验证码已发送,请检查 ${formModel.mobilePhone} 手机短信`);
|
Message.success(`验证码已发送,请检查 ${formModel.mobilePhone} 手机短信(易保网络)`, 5000);
|
||||||
} catch {
|
} catch {
|
||||||
formModel.notifyDetailId = "";
|
formModel.notifyDetailId = "";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -136,11 +136,11 @@ async function sendSms(): Promise<void> {
|
|||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
if (!/^1\d{10}$/.test(formModel.mobilePhone)) {
|
if (!/^1\d{10}$/.test(formModel.mobilePhone)) {
|
||||||
Toast.warning("请输入正确的 11 位手机号");
|
Message.warning("请输入正确的 11 位手机号");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!formModel.notifyDetailId || !formModel.smsCode) {
|
if (!formModel.notifyDetailId || !formModel.smsCode) {
|
||||||
Toast.warning("请先获取并输入短信验证码");
|
Message.warning("请先获取并输入短信验证码");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -14,14 +14,6 @@
|
|||||||
<button class="close" type="button" @click="popupVisible = false">×</button>
|
<button class="close" type="button" @click="popupVisible = false">×</button>
|
||||||
</header>
|
</header>
|
||||||
<t-form class="form" label-align="left">
|
<t-form class="form" label-align="left">
|
||||||
<t-form-item
|
|
||||||
v-if="isGlobalManager"
|
|
||||||
label="目标门店"
|
|
||||||
arrow
|
|
||||||
@click="openPicker('store')"
|
|
||||||
>
|
|
||||||
<span v-text="selectedStoreName" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item
|
<t-form-item
|
||||||
label="目标角色"
|
label="目标角色"
|
||||||
arrow
|
arrow
|
||||||
@@ -29,6 +21,14 @@
|
|||||||
>
|
>
|
||||||
<span v-text="selectedRoleName" />
|
<span v-text="selectedRoleName" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
|
<t-form-item
|
||||||
|
v-if="shouldSelectStore"
|
||||||
|
label="目标门店"
|
||||||
|
arrow
|
||||||
|
@click="openPicker('store')"
|
||||||
|
>
|
||||||
|
<span v-text="selectedStoreName" />
|
||||||
|
</t-form-item>
|
||||||
</t-form>
|
</t-form>
|
||||||
</section>
|
</section>
|
||||||
</t-dialog>
|
</t-dialog>
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { gaoRoleCodes } from "@/utils/permission";
|
import { coreRoleCodes, gaoRoleCodes } from "@/utils/permission";
|
||||||
import { RoleAPI, useUserStore } from "timi-web";
|
import { RoleAPI, type Role, useUserStore } from "timi-web";
|
||||||
|
|
||||||
defineOptions({ name: "UserInvite" });
|
defineOptions({ name: "UserInvite" });
|
||||||
|
|
||||||
@@ -62,17 +62,20 @@ const emit = defineEmits<{
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const currentStoreStore = useCurrentStoreStore();
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
const hasGlobalStoreScope = computed(() => userStore.hasRoles([coreRoleCodes.admin, gaoRoleCodes.globalManager], "or"));
|
||||||
const popupVisible = computed({
|
const popupVisible = computed({
|
||||||
get: () => props.visible,
|
get: () => props.visible,
|
||||||
set: value => emit("update:visible", value)
|
set: value => emit("update:visible", value)
|
||||||
});
|
});
|
||||||
const formModel = reactive({ roleId: "", storeId: "" });
|
const formModel = reactive({ roleId: "", storeId: "" });
|
||||||
const roleList = ref<Array<{ id?: string; name?: string; code?: string }>>([]);
|
const roleList = ref<Role[]>([]);
|
||||||
const isGenerating = ref(false);
|
const isGenerating = ref(false);
|
||||||
const isLoaded = ref(false);
|
const isLoaded = ref(false);
|
||||||
const roleOptions = computed(() => roleList.value.filter(item => item.id).map(item => ({ label: item.name || item.code || "未命名角色", value: item.id as string })));
|
const roleOptions = computed(() => roleList.value.filter(item => item.id).map(item => ({ label: item.name || item.code || "未命名角色", value: item.id as string })));
|
||||||
const storeOptions = computed(() => currentStoreStore.storeList.filter(item => item.id).map(item => ({ label: item.name || item.code || "未命名门店", value: item.id as string })));
|
const storeOptions = computed(() => currentStoreStore.storeList.filter(item => item.id).map(item => ({ label: item.name || item.code || "未命名门店", value: item.id as string })));
|
||||||
|
const selectedRole = computed(() => roleList.value.find(item => item.id === formModel.roleId));
|
||||||
|
const isGlobalManagerRole = computed(() => selectedRole.value?.code === "GLOBAL_MANAGER");
|
||||||
|
const shouldSelectStore = computed(() => hasGlobalStoreScope.value && !!selectedRole.value && !isGlobalManagerRole.value);
|
||||||
const selectedRoleName = computed(() => roleOptions.value.find(item => item.value === formModel.roleId)?.label || "请选择角色");
|
const selectedRoleName = computed(() => roleOptions.value.find(item => item.value === formModel.roleId)?.label || "请选择角色");
|
||||||
const selectedStoreName = computed(() => storeOptions.value.find(item => item.value === formModel.storeId)?.label || "请选择门店");
|
const selectedStoreName = computed(() => storeOptions.value.find(item => item.value === formModel.storeId)?.label || "请选择门店");
|
||||||
const pickerVisible = ref(false);
|
const pickerVisible = ref(false);
|
||||||
@@ -85,14 +88,14 @@ async function loadData(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const roleRequest = RoleAPI.manageableList("GAO");
|
const roleRequest = RoleAPI.manageableList("GAO");
|
||||||
if (isGlobalManager.value) {
|
if (hasGlobalStoreScope.value) {
|
||||||
await currentStoreStore.loadStoreList();
|
await currentStoreStore.loadStoreList();
|
||||||
formModel.storeId = currentStoreStore.storeList.some(item => item.id === props.storeId)
|
formModel.storeId = currentStoreStore.storeList.some(item => item.id === props.storeId)
|
||||||
? props.storeId || ""
|
? props.storeId || ""
|
||||||
: currentStoreStore.currentStoreId;
|
: currentStoreStore.currentStoreId;
|
||||||
}
|
}
|
||||||
roleList.value = await roleRequest;
|
roleList.value = await roleRequest;
|
||||||
if (!isGlobalManager.value) {
|
if (!hasGlobalStoreScope.value) {
|
||||||
const store = await currentStoreStore.loadLoginStore();
|
const store = await currentStoreStore.loadLoginStore();
|
||||||
formModel.storeId = store?.id || "";
|
formModel.storeId = store?.id || "";
|
||||||
}
|
}
|
||||||
@@ -109,6 +112,9 @@ function confirmPicker(): void {
|
|||||||
const value = String(pickerValue.value[0] || "");
|
const value = String(pickerValue.value[0] || "");
|
||||||
if (pickerType.value === "role") {
|
if (pickerType.value === "role") {
|
||||||
formModel.roleId = value;
|
formModel.roleId = value;
|
||||||
|
if (isGlobalManagerRole.value) {
|
||||||
|
formModel.storeId = "";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
formModel.storeId = value;
|
formModel.storeId = value;
|
||||||
}
|
}
|
||||||
@@ -116,20 +122,20 @@ function confirmPicker(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateInvite(): Promise<void> {
|
async function generateInvite(): Promise<void> {
|
||||||
if (!formModel.roleId || (isGlobalManager.value && !formModel.storeId)) {
|
if (!formModel.roleId || (shouldSelectStore.value && !formModel.storeId)) {
|
||||||
Toast.warning(isGlobalManager.value ? "请选择角色和门店" : "请选择角色");
|
Toast.warning(shouldSelectStore.value ? "请选择角色和门店" : "请选择角色");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isGenerating.value = true;
|
isGenerating.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await ManagedUserAPI.invite(isGlobalManager.value ? { roleId: formModel.roleId, storeId: formModel.storeId } : { roleId: formModel.roleId });
|
const result = await ManagedUserAPI.invite(shouldSelectStore.value ? { roleId: formModel.roleId, storeId: formModel.storeId } : { roleId: formModel.roleId });
|
||||||
popupVisible.value = false;
|
popupVisible.value = false;
|
||||||
await router.push({
|
await router.push({
|
||||||
name: "UserInvitePoster",
|
name: "UserInvitePoster",
|
||||||
query: {
|
query: {
|
||||||
code: result.code,
|
code: result.code,
|
||||||
expireAt: String(result.expireAt),
|
expireAt: String(result.expireAt),
|
||||||
storeName: isGlobalManager.value ? selectedStoreName.value : currentStoreStore.currentStoreName,
|
storeName: isGlobalManagerRole.value ? "糕店客户管理系统" : shouldSelectStore.value ? selectedStoreName.value : currentStoreStore.currentStoreName,
|
||||||
roleName: selectedRoleName.value,
|
roleName: selectedRoleName.value,
|
||||||
inviterName: getCurrentUserName()
|
inviterName: getCurrentUserName()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ import { createRouter, createWebHistory, type RouteLocationNormalized } from "vu
|
|||||||
import RootLayout from "@/layout/RootLayout.vue";
|
import RootLayout from "@/layout/RootLayout.vue";
|
||||||
import MainLayout from "@/layout/MainLayout.vue";
|
import MainLayout from "@/layout/MainLayout.vue";
|
||||||
import Login from "@/pages/user/LoginIndex.vue";
|
import Login from "@/pages/user/LoginIndex.vue";
|
||||||
import InviteRegister from "@/pages/InviteRegister.vue";
|
import InviteRegister from "@/pages/user/InviteRegister.vue";
|
||||||
import chartRoutes from "@/router/routes/chartRoutes";
|
import chartRoutes from "@/router/routes/chartRoutes";
|
||||||
import customerRoutes from "@/router/routes/customerRoutes";
|
import customerRoutes from "@/router/routes/customerRoutes";
|
||||||
import { managementAccountRoutes, managementSettingRoutes, storeRoutes } from "@/router/routes/managementRoutes";
|
import { managementAccountRoutes, managementSettingRoutes, storeRoutes } from "@/router/routes/managementRoutes";
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ html.theme-dark body {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-button {
|
||||||
|
--td-button-border-radius: 1rem;
|
||||||
|
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-input--password {
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.t-form__label {
|
.t-form__label {
|
||||||
color: #555;
|
color: #555;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { MessagePlugin as TDesignMessage } from "tdesign-mobile-vue";
|
||||||
|
import type { MessagePluginOptions } from "tdesign-mobile-vue/es/message/message";
|
||||||
|
|
||||||
|
/** Message 支持的调用参数。 */
|
||||||
|
export type MessageOptions = MessagePluginOptions;
|
||||||
|
export type MessageInput = string | MessageOptions;
|
||||||
|
|
||||||
|
type MessageTheme = Exclude<NonNullable<MessageOptions["theme"]>, "loading">;
|
||||||
|
|
||||||
|
interface TDesignMessageApi {
|
||||||
|
info: (options: MessageOptions) => void;
|
||||||
|
success: (options: MessageOptions) => void;
|
||||||
|
warning: (options: MessageOptions) => void;
|
||||||
|
error: (options: MessageOptions) => void;
|
||||||
|
closeAll: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tDesignMessage = TDesignMessage as unknown as TDesignMessageApi;
|
||||||
|
|
||||||
|
function normalizeOptions(input: MessageInput): MessageOptions {
|
||||||
|
return typeof input === "string" ? { content: input } : input;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptionsWithDuration(input: MessageInput, duration?: number): MessageOptions {
|
||||||
|
const options = normalizeOptions(input);
|
||||||
|
return duration === undefined ? options : { ...options, duration };
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(input: MessageInput, duration?: number): void {
|
||||||
|
const options = normalizeOptionsWithDuration(input, duration);
|
||||||
|
const theme = options.theme || "info";
|
||||||
|
tDesignMessage[theme](options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessageByTheme(theme: MessageTheme, input: MessageInput, duration?: number): void {
|
||||||
|
tDesignMessage[theme]({
|
||||||
|
offset: [10, 16],
|
||||||
|
...normalizeOptionsWithDuration(input, duration),
|
||||||
|
theme
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一 Message 调用方式,使其支持类似 Toast 的字符串和配置对象调用。
|
||||||
|
*/
|
||||||
|
export const Message = Object.assign(showMessage, {
|
||||||
|
info: (input: MessageInput, duration?: number): void => showMessageByTheme("info", input, duration),
|
||||||
|
success: (input: MessageInput, duration?: number): void => showMessageByTheme("success", input, duration),
|
||||||
|
warning: (input: MessageInput, duration?: number): void => showMessageByTheme("warning", input, duration),
|
||||||
|
error: (input: MessageInput, duration?: number): void => showMessageByTheme("error", input, duration),
|
||||||
|
closeAll: (): void => tDesignMessage.closeAll()
|
||||||
|
});
|
||||||
+2
-2
@@ -209,7 +209,7 @@ function renderInvitePoster(options: InvitePosterOptions): HTMLCanvasElement {
|
|||||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
context.fillStyle = teal;
|
context.fillStyle = teal;
|
||||||
context.fillRect(0, 0, canvas.width, 640);
|
context.fillRect(0, 0, canvas.width, 640);
|
||||||
context.fillStyle = "#CCFBF1";
|
context.fillStyle = "#6eb0a5";
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.arc(1140, 40, 220, 0, Math.PI * 2);
|
context.arc(1140, 40, 220, 0, Math.PI * 2);
|
||||||
context.fill();
|
context.fill();
|
||||||
@@ -250,7 +250,7 @@ function renderInvitePoster(options: InvitePosterOptions): HTMLCanvasElement {
|
|||||||
context.fillText("客户档案 | 事件登记 | 积分激励 | 门店协同 | AI 分析", 600, 1645);
|
context.fillText("客户档案 | 事件登记 | 积分激励 | 门店协同 | AI 分析", 600, 1645);
|
||||||
context.fillStyle = "#94A3B8";
|
context.fillStyle = "#94A3B8";
|
||||||
context.font = "400 40px Microsoft YaHei, sans-serif";
|
context.font = "400 40px Microsoft YaHei, sans-serif";
|
||||||
context.fillText("本邀请仅限本人使用,请勿转发", 600, 1760);
|
context.fillText("本邀请码为一次性使用", 600, 1760);
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user