update user invite style

This commit is contained in:
Timi
2026-08-22 00:53:39 +08:00
parent 7ff8ed7f3d
commit 94c17e6d81
12 changed files with 285 additions and 133 deletions
+1 -1
View File
@@ -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,61 +287,29 @@ 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; margin-top: .5rem;
align-items: center; justify-content: end;
.t-input { .captcha {
min-width: 0; border: 1px solid var(--td-border-level-1-color);
flex: 1; }
} }
}
:deep(.tui-captcha) { .send-sms {
flex: 0 0 7.5rem; flex: 0 0 7.5rem;
border: 1px solid var(--td-border-level-1-color); }
} }
.submit { .submit {
+2 -17
View File
@@ -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) {
+18 -8
View File
@@ -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;
} }
} }
+3 -8
View File
@@ -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) {
+25
View File
@@ -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>
+5 -5
View File
@@ -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;
+24 -18
View File
@@ -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
View File
@@ -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";
+11
View File
@@ -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;
+52
View File
@@ -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
View File
@@ -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;
} }