feat(admin-web/auth): support phone-or-email login and dim shell on expiry
Login form takes a phone-or-email identifier; user info now exposes phone so the shell can fall back through name → phone → email. When membership expires, the app stays on the current route but renders a minimal blocked state in the shell instead of redirecting to a dedicated page; an Axios interceptor flips the local session into the blocked state when the API returns trial/subscription errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,7 @@ const enUS = {
|
|||||||
tenantAdminLogin: "Tenant Admin Login",
|
tenantAdminLogin: "Tenant Admin Login",
|
||||||
loginAndEnter: "Sign in to workspace",
|
loginAndEnter: "Sign in to workspace",
|
||||||
email: "Email",
|
email: "Email",
|
||||||
|
loginIdentifier: "Phone / Email",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
defaultTestAccount: "Default account",
|
defaultTestAccount: "Default account",
|
||||||
defaultTestPassword: "Default password",
|
defaultTestPassword: "Default password",
|
||||||
@@ -101,6 +102,7 @@ const enUS = {
|
|||||||
membershipBlocked: {
|
membershipBlocked: {
|
||||||
eyebrow: "Access Paused",
|
eyebrow: "Access Paused",
|
||||||
title: "This tenant plan is unavailable",
|
title: "This tenant plan is unavailable",
|
||||||
|
expiredMessage: "Membership expired. Please renew or contact support.",
|
||||||
reasonTrialExpired: "The free trial's 3-day page access window has ended. All business pages are now locked. Please contact an administrator to reactivate trial access.",
|
reasonTrialExpired: "The free trial's 3-day page access window has ended. All business pages are now locked. Please contact an administrator to reactivate trial access.",
|
||||||
reasonRequired: "This tenant does not have an active plan. All business pages are now locked. Please contact an administrator.",
|
reasonRequired: "This tenant does not have an active plan. All business pages are now locked. Please contact an administrator.",
|
||||||
reasonInactive: "This tenant plan has expired or been disabled. All business pages are now locked. Please contact an administrator to restore access.",
|
reasonInactive: "This tenant plan has expired or been disabled. All business pages are now locked. Please contact an administrator to restore access.",
|
||||||
@@ -118,6 +120,7 @@ const enUS = {
|
|||||||
localeLabel: "Language",
|
localeLabel: "Language",
|
||||||
quotaTitle: "Plan & Quota",
|
quotaTitle: "Plan & Quota",
|
||||||
quotaStatus: "Healthy",
|
quotaStatus: "Healthy",
|
||||||
|
membershipExpiresAt: "Expires",
|
||||||
remainingQuota: "Article quota",
|
remainingQuota: "Article quota",
|
||||||
aiPoints: "AI points",
|
aiPoints: "AI points",
|
||||||
resetAt: "Reset at",
|
resetAt: "Reset at",
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ const zhCN = {
|
|||||||
tenantAdminLogin: "租户后台登录",
|
tenantAdminLogin: "租户后台登录",
|
||||||
loginAndEnter: "登录并进入工作台",
|
loginAndEnter: "登录并进入工作台",
|
||||||
email: "邮箱",
|
email: "邮箱",
|
||||||
|
loginIdentifier: "手机号 / 邮箱",
|
||||||
password: "密码",
|
password: "密码",
|
||||||
defaultTestAccount: "默认测试账号",
|
defaultTestAccount: "默认测试账号",
|
||||||
defaultTestPassword: "默认测试密码",
|
defaultTestPassword: "默认测试密码",
|
||||||
@@ -101,6 +102,7 @@ const zhCN = {
|
|||||||
membershipBlocked: {
|
membershipBlocked: {
|
||||||
eyebrow: "访问已暂停",
|
eyebrow: "访问已暂停",
|
||||||
title: "当前租户套餐不可用",
|
title: "当前租户套餐不可用",
|
||||||
|
expiredMessage: "会员到期,请充值或联系客服",
|
||||||
reasonTrialExpired: "免费试用的 3 天页面可访问期已经结束,当前业务页面已全部关闭,请联系管理员重新开通试用权限。",
|
reasonTrialExpired: "免费试用的 3 天页面可访问期已经结束,当前业务页面已全部关闭,请联系管理员重新开通试用权限。",
|
||||||
reasonRequired: "当前租户尚未开通有效套餐,业务页面已全部关闭,请联系管理员处理。",
|
reasonRequired: "当前租户尚未开通有效套餐,业务页面已全部关闭,请联系管理员处理。",
|
||||||
reasonInactive: "当前租户套餐已失效或已停用,业务页面已全部关闭,请联系管理员恢复访问。",
|
reasonInactive: "当前租户套餐已失效或已停用,业务页面已全部关闭,请联系管理员恢复访问。",
|
||||||
@@ -118,6 +120,7 @@ const zhCN = {
|
|||||||
localeLabel: "语言",
|
localeLabel: "语言",
|
||||||
quotaTitle: "套餐与额度",
|
quotaTitle: "套餐与额度",
|
||||||
quotaStatus: "状态正常",
|
quotaStatus: "状态正常",
|
||||||
|
membershipExpiresAt: "会员到期",
|
||||||
remainingQuota: "生成文章",
|
remainingQuota: "生成文章",
|
||||||
aiPoints: "AI 点数",
|
aiPoints: "AI 点数",
|
||||||
resetAt: "重置时间",
|
resetAt: "重置时间",
|
||||||
|
|||||||
@@ -24,13 +24,37 @@ const router = useRouter();
|
|||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
|
|
||||||
|
const isMembershipBlocked = computed(() => authStore.isMembershipBlocked);
|
||||||
|
|
||||||
const quotaQuery = useQuery({
|
const quotaQuery = useQuery({
|
||||||
queryKey: ["workspace", "quota-summary", "shell"],
|
queryKey: ["workspace", "quota-summary", "shell"],
|
||||||
queryFn: () => workspaceApi.quotaSummary(),
|
queryFn: () => workspaceApi.quotaSummary(),
|
||||||
|
enabled: computed(() => !isMembershipBlocked.value),
|
||||||
});
|
});
|
||||||
|
|
||||||
const userInitial = computed(() => authStore.user?.name?.slice(0, 1).toUpperCase() ?? "A");
|
const userDisplayName = computed(() => {
|
||||||
|
return authStore.user?.name || authStore.user?.phone || authStore.user?.email || t("shell.userFallback");
|
||||||
|
});
|
||||||
|
const userSecondaryIdentifier = computed(() => authStore.user?.email || authStore.user?.phone || "--");
|
||||||
|
const userInitial = computed(() => userDisplayName.value.slice(0, 1).toUpperCase() || "A");
|
||||||
const quotaSummary = computed(() => quotaQuery.data.value);
|
const quotaSummary = computed(() => quotaQuery.data.value);
|
||||||
|
const membershipExpiryText = computed(() => {
|
||||||
|
const value = authStore.membership?.end_at;
|
||||||
|
if (!value) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(locale.value, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).format(date);
|
||||||
|
});
|
||||||
const selectedKeys = computed(() => {
|
const selectedKeys = computed(() => {
|
||||||
if (route.meta.navKey === null) {
|
if (route.meta.navKey === null) {
|
||||||
return [];
|
return [];
|
||||||
@@ -242,9 +266,9 @@ async function handleLogout(): Promise<void> {
|
|||||||
</a-menu>
|
</a-menu>
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
|
|
||||||
<a-layout class="admin-main-layout">
|
<a-layout class="admin-main-layout" :class="{ 'admin-main-layout--blocked': isMembershipBlocked }">
|
||||||
<a-layout-header class="admin-header">
|
<a-layout-header class="admin-header" :class="{ 'admin-header--blocked': isMembershipBlocked }">
|
||||||
<nav class="admin-header-breadcrumb" aria-label="Breadcrumb">
|
<nav v-if="!isMembershipBlocked" class="admin-header-breadcrumb" aria-label="Breadcrumb">
|
||||||
<span
|
<span
|
||||||
v-for="(crumb, index) in headerBreadcrumbs"
|
v-for="(crumb, index) in headerBreadcrumbs"
|
||||||
:key="`${crumb.label}-${index}`"
|
:key="`${crumb.label}-${index}`"
|
||||||
@@ -269,7 +293,7 @@ async function handleLogout(): Promise<void> {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="admin-header-actions">
|
<div class="admin-header-actions">
|
||||||
<div class="admin-locale-switch">
|
<div v-if="!isMembershipBlocked" class="admin-locale-switch">
|
||||||
<a-dropdown placement="bottomRight" :trigger="['click', 'hover']">
|
<a-dropdown placement="bottomRight" :trigger="['click', 'hover']">
|
||||||
<div class="locale-dropdown-trigger">
|
<div class="locale-dropdown-trigger">
|
||||||
<GlobalOutlined class="locale-icon" />
|
<GlobalOutlined class="locale-icon" />
|
||||||
@@ -286,9 +310,11 @@ async function handleLogout(): Promise<void> {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quota Indicator -->
|
<!-- Quota Indicator -->
|
||||||
<div class="quota-pill">
|
<div v-if="!isMembershipBlocked" class="quota-pill">
|
||||||
<a-tag color="blue" :bordered="false" class="quota-pill-tag">{{ quotaSummary?.plan_name || t("shell.planFallback") }}</a-tag>
|
<a-tag color="blue" :bordered="false" class="quota-pill-tag">{{ quotaSummary?.plan_name || t("shell.planFallback") }}</a-tag>
|
||||||
<span class="quota-pill-text">
|
<span class="quota-pill-text">
|
||||||
|
{{ t("shell.membershipExpiresAt") }}: <strong>{{ membershipExpiryText }}</strong>
|
||||||
|
<span class="quota-pill-divider">·</span>
|
||||||
{{ t("shell.remainingQuota") }}: <strong>{{ quotaSummary?.balance ?? '--' }}</strong>
|
{{ t("shell.remainingQuota") }}: <strong>{{ quotaSummary?.balance ?? '--' }}</strong>
|
||||||
<span class="quota-pill-total">/ {{ quotaSummary?.total_quota ?? '--' }}</span>
|
<span class="quota-pill-total">/ {{ quotaSummary?.total_quota ?? '--' }}</span>
|
||||||
<span class="quota-pill-divider">·</span>
|
<span class="quota-pill-divider">·</span>
|
||||||
@@ -301,17 +327,17 @@ async function handleLogout(): Promise<void> {
|
|||||||
<a-dropdown placement="bottomRight" :trigger="['click', 'hover']">
|
<a-dropdown placement="bottomRight" :trigger="['click', 'hover']">
|
||||||
<div class="user-dropdown-trigger">
|
<div class="user-dropdown-trigger">
|
||||||
<a-avatar size="small" class="user-avatar">{{ userInitial }}</a-avatar>
|
<a-avatar size="small" class="user-avatar">{{ userInitial }}</a-avatar>
|
||||||
<span class="user-name">{{ authStore.user?.name || t("shell.userFallback") }}</span>
|
<span class="user-name">{{ userDisplayName }}</span>
|
||||||
<DownOutlined class="user-dropdown-icon" />
|
<DownOutlined class="user-dropdown-icon" />
|
||||||
</div>
|
</div>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu class="user-dropdown-menu">
|
<a-menu class="user-dropdown-menu">
|
||||||
<div class="user-dropdown-header">
|
<div class="user-dropdown-header">
|
||||||
<span class="user-dropdown-name">{{ authStore.user?.name || t("shell.userFallback") }}</span>
|
<span class="user-dropdown-name">{{ userDisplayName }}</span>
|
||||||
<span class="user-dropdown-role">{{ authStore.user?.email || "--" }}</span>
|
<span class="user-dropdown-role">{{ userSecondaryIdentifier }}</span>
|
||||||
</div>
|
</div>
|
||||||
<a-menu-divider />
|
<a-menu-divider />
|
||||||
<a-menu-item key="workspace" @click="go('/workspace')" class="user-dropdown-item">
|
<a-menu-item v-if="!isMembershipBlocked" key="workspace" @click="go('/workspace')" class="user-dropdown-item">
|
||||||
{{ t("nav.workspace") }}
|
{{ t("nav.workspace") }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="logout" @click="handleLogout" class="user-dropdown-item user-dropdown-logout">
|
<a-menu-item key="logout" @click="handleLogout" class="user-dropdown-item user-dropdown-logout">
|
||||||
@@ -324,8 +350,17 @@ async function handleLogout(): Promise<void> {
|
|||||||
</div>
|
</div>
|
||||||
</a-layout-header>
|
</a-layout-header>
|
||||||
|
|
||||||
<a-layout-content class="admin-content">
|
<a-layout-content class="admin-content" :class="{ 'admin-content--blocked': isMembershipBlocked }">
|
||||||
<router-view />
|
<section
|
||||||
|
v-if="isMembershipBlocked"
|
||||||
|
class="membership-expired-state"
|
||||||
|
role="alert"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<div class="membership-expired-logo" :aria-label="t('app.name')">GEO</div>
|
||||||
|
<p>{{ t("membershipBlocked.expiredMessage") }}</p>
|
||||||
|
</section>
|
||||||
|
<router-view v-else />
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
@@ -352,6 +387,10 @@ async function handleLogout(): Promise<void> {
|
|||||||
margin-left: 250px;
|
margin-left: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-main-layout--blocked {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-brand {
|
.admin-brand {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: #f0f8ff;
|
background: #f0f8ff;
|
||||||
@@ -384,6 +423,10 @@ async function handleLogout(): Promise<void> {
|
|||||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-header--blocked {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-header-breadcrumb {
|
.admin-header-breadcrumb {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -436,6 +479,7 @@ async function handleLogout(): Promise<void> {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-locale-switch {
|
.admin-locale-switch {
|
||||||
@@ -574,4 +618,45 @@ async function handleLogout(): Promise<void> {
|
|||||||
margin: 24px;
|
margin: 24px;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-content--blocked {
|
||||||
|
margin: 0;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-expired-state {
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 32px;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-expired-logo {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #1677ff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0;
|
||||||
|
box-shadow: 0 14px 32px rgba(22, 119, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-expired-state p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ import {
|
|||||||
clearStoredSession,
|
clearStoredSession,
|
||||||
getStoredAccessToken,
|
getStoredAccessToken,
|
||||||
getStoredRefreshToken,
|
getStoredRefreshToken,
|
||||||
|
markStoredMembershipBlocked,
|
||||||
readStoredSession,
|
readStoredSession,
|
||||||
setStoredTokens,
|
setStoredTokens,
|
||||||
} from "./session";
|
} from "./session";
|
||||||
@@ -271,6 +272,23 @@ export const apiClient = createApiClient({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const membershipBlockedErrors = new Set([
|
||||||
|
"trial_plan_expired",
|
||||||
|
"subscription_required",
|
||||||
|
"subscription_inactive",
|
||||||
|
]);
|
||||||
|
|
||||||
|
apiClient.raw.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error instanceof ApiClientError && membershipBlockedErrors.has(error.message)) {
|
||||||
|
markStoredMembershipBlocked(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
login(payload: LoginRequest) {
|
login(payload: LoginRequest) {
|
||||||
return publicClient.post<LoginResponse, LoginRequest>("/api/auth/login", payload);
|
return publicClient.post<LoginResponse, LoginRequest>("/api/auth/login", payload);
|
||||||
|
|||||||
@@ -95,6 +95,36 @@ export function setStoredUser(user: UserInfo | null): SessionSnapshot {
|
|||||||
return updateStoredSession({ user });
|
return updateStoredSession({ user });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function markStoredMembershipBlocked(reason = "subscription_inactive"): SessionSnapshot {
|
||||||
|
const snapshot = readStoredSession();
|
||||||
|
if (!snapshot.user) {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = snapshot.user.membership;
|
||||||
|
const nextUser: UserInfo = {
|
||||||
|
...snapshot.user,
|
||||||
|
membership: {
|
||||||
|
plan_code: current?.plan_code ?? "",
|
||||||
|
plan_name: current?.plan_name ?? "",
|
||||||
|
status: "expired",
|
||||||
|
blocked: true,
|
||||||
|
blocked_reason: reason,
|
||||||
|
start_at: current?.start_at ?? null,
|
||||||
|
end_at: current?.end_at ?? null,
|
||||||
|
article_generation_limit: current?.article_generation_limit ?? 0,
|
||||||
|
article_quota_cycle: current?.article_quota_cycle ?? "lifetime",
|
||||||
|
ai_points_monthly: current?.ai_points_monthly ?? 0,
|
||||||
|
ai_point_base_chars: current?.ai_point_base_chars ?? 1000,
|
||||||
|
company_limit: current?.company_limit ?? 0,
|
||||||
|
image_storage_bytes: current?.image_storage_bytes ?? 0,
|
||||||
|
contact_admin_on_expiry: current?.contact_admin_on_expiry ?? true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return updateStoredSession({ user: nextUser });
|
||||||
|
}
|
||||||
|
|
||||||
export function clearStoredSession(options: { notifyAuthExpired?: boolean } = {}): SessionSnapshot {
|
export function clearStoredSession(options: { notifyAuthExpired?: boolean } = {}): SessionSnapshot {
|
||||||
if (hasStorage()) {
|
if (hasStorage()) {
|
||||||
window.localStorage.removeItem(STORAGE_KEY);
|
window.localStorage.removeItem(STORAGE_KEY);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: "/membership-blocked",
|
path: "/membership-blocked",
|
||||||
name: "membership-blocked",
|
name: "membership-blocked",
|
||||||
component: () => import("@/views/MembershipBlockedView.vue"),
|
redirect: "/workspace",
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
@@ -261,22 +261,11 @@ router.beforeEach(async (to) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authStore.isAuthenticated && authStore.isMembershipBlocked && to.name !== "membership-blocked") {
|
|
||||||
return { name: "membership-blocked" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to.name === "membership-blocked" && authStore.isAuthenticated && !authStore.isMembershipBlocked) {
|
|
||||||
return { name: "workspace" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to.meta.requiresKol && !authStore.isActiveKol) {
|
if (to.meta.requiresKol && !authStore.isActiveKol) {
|
||||||
return { name: "workspace" };
|
return { name: "workspace" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.name === "login" && authStore.isAuthenticated) {
|
if (to.name === "login" && authStore.isAuthenticated) {
|
||||||
if (authStore.isMembershipBlocked) {
|
|
||||||
return { name: "membership-blocked" };
|
|
||||||
}
|
|
||||||
return { name: "workspace" };
|
return { name: "workspace" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,16 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
|
|
||||||
const isAuthenticated = computed(() => Boolean(accessToken.value && user.value));
|
const isAuthenticated = computed(() => Boolean(accessToken.value && user.value));
|
||||||
const membership = computed(() => user.value?.membership ?? null);
|
const membership = computed(() => user.value?.membership ?? null);
|
||||||
const isMembershipBlocked = computed(() => Boolean(membership.value?.blocked));
|
const isMembershipBlocked = computed(() => {
|
||||||
|
const value = membership.value;
|
||||||
|
return Boolean(
|
||||||
|
value?.blocked ||
|
||||||
|
value?.status === "expired" ||
|
||||||
|
value?.blocked_reason === "trial_plan_expired" ||
|
||||||
|
value?.blocked_reason === "subscription_required" ||
|
||||||
|
value?.blocked_reason === "subscription_inactive",
|
||||||
|
);
|
||||||
|
});
|
||||||
const kolProfile = computed(() => user.value?.kol_profile ?? null);
|
const kolProfile = computed(() => user.value?.kol_profile ?? null);
|
||||||
const isActiveKol = computed(() => kolProfile.value?.status === "active");
|
const isActiveKol = computed(() => kolProfile.value?.status === "active");
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="handleSubmit" class="form">
|
<form @submit.prevent="handleSubmit" class="form">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="login-email">{{ t('auth.email') }}</label>
|
<label for="login-identifier">{{ t('auth.loginIdentifier') }}</label>
|
||||||
<input id="login-email" type="email" placeholder="admin@geo.local" v-model="formState.email"
|
<input id="login-identifier" type="text" placeholder="admin@geo.local / 13800138000" v-model="formState.identifier"
|
||||||
autocomplete="off" @focus="isTyping = true" @blur="isTyping = false" required />
|
autocomplete="off" @focus="isTyping = true" @blur="isTyping = false" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -98,7 +98,7 @@ const isTyping = ref(false);
|
|||||||
const remember = ref(false);
|
const remember = ref(false);
|
||||||
|
|
||||||
const formState = reactive({
|
const formState = reactive({
|
||||||
email: "admin@geo.local",
|
identifier: "admin@geo.local",
|
||||||
password: "Admin@123",
|
password: "Admin@123",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ const expiryText = computed(() => {
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
}).format(date);
|
}).format(date);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,16 @@ export interface AuthTokens {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
email: string;
|
identifier: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string | null;
|
||||||
name: string;
|
phone: string;
|
||||||
|
name: string | null;
|
||||||
avatar_url: string | null;
|
avatar_url: string | null;
|
||||||
tenant_id: number;
|
tenant_id: number;
|
||||||
primary_workspace_id: number;
|
primary_workspace_id: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user