调整账号注册与删除流程
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { apiGet, apiPost } from "./axios";
|
||||
import type { AdminUserListResponse, UserInfo, UserStatus } from "../types/api";
|
||||
|
||||
export const listPendingUsers = (status: UserStatus = "PENDING"): Promise<AxiosResponse<AdminUserListResponse>> =>
|
||||
apiGet("/api/v1/admin/users/", { params: { status } });
|
||||
|
||||
export const approveUser = (userId: string): Promise<AxiosResponse<UserInfo>> =>
|
||||
apiPost(`/api/v1/admin/users/${userId}/approve`, { action: "approve" });
|
||||
|
||||
export const rejectUser = (userId: string): Promise<AxiosResponse<UserInfo>> =>
|
||||
apiPost(`/api/v1/admin/users/${userId}/reject`, { action: "reject" });
|
||||
|
||||
@@ -20,4 +20,4 @@ export const updateUser = (
|
||||
apiPatch<UserInfo>(`/api/v1/users/${userId}`, payload);
|
||||
|
||||
export const deleteUser = (userId: string) =>
|
||||
apiDelete<void>(`/api/v1/users/${userId}`);
|
||||
apiDelete<void>(`/api/v1/users/${userId}`, { suppressErrorMessage: true });
|
||||
|
||||
@@ -52,7 +52,7 @@ export const privacyPolicySections: AuthProtocolSection[] = [
|
||||
{
|
||||
title: "2. 数据使用与最小必要",
|
||||
paragraphs: [
|
||||
"平台仅在账号注册审核、身份识别、权限分配、项目协同、风险追踪、审计核查和系统安全维护所需范围内使用相关数据。",
|
||||
"平台仅在账号注册、身份识别、权限分配、项目协同、风险追踪、审计核查和系统安全维护所需范围内使用相关数据。",
|
||||
"用户应遵循最小必要原则录入、查看、导出和传播数据,不得上传与当前项目管理无关的个人信息、受试者信息或其他敏感资料。",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -329,9 +329,9 @@ export const TEXT = {
|
||||
footer: "© 2025 CTMS Enterprise. 专业临床研究团队支持。",
|
||||
authFailed: "认证失败,请检查您的凭据",
|
||||
registerTitle: "创建账号",
|
||||
registerDesc: "提交后需管理员审核通过方可登录",
|
||||
registerButton: "提交注册",
|
||||
registerSuccess: "注册成功,等待管理员审核",
|
||||
registerDesc: "完成邮箱验证后即可创建可用账号",
|
||||
registerButton: "完成注册",
|
||||
registerSuccess: "注册成功,请登录",
|
||||
backToLogin: "已有账号?返回登录",
|
||||
passwordRuleMin: "密码至少 8 位",
|
||||
passwordRuleMix: "需包含字母和数字",
|
||||
@@ -775,18 +775,6 @@ export const TEXT = {
|
||||
siteRequired: "中心级目录需选择分中心",
|
||||
},
|
||||
},
|
||||
adminUserApproval: {
|
||||
title: "注册审核",
|
||||
subtitle: "仅显示待审核账号,审核通过后即可登录",
|
||||
createdAt: "注册时间",
|
||||
approve: "审核通过",
|
||||
reject: "拒绝",
|
||||
loadFailed: "加载待审核用户失败",
|
||||
approveSuccess: "已通过审核",
|
||||
approveFailed: "审核失败",
|
||||
rejectSuccess: "已拒绝",
|
||||
rejectFailed: "拒绝失败",
|
||||
},
|
||||
adminUsers: {
|
||||
title: "账号管理(系统登录账号)",
|
||||
subtitle: "账号用于登录系统,本身不具备项目或角色权限",
|
||||
@@ -994,9 +982,7 @@ export const TEXT = {
|
||||
CTA: "CTA",
|
||||
},
|
||||
userStatus: {
|
||||
PENDING: "待审核",
|
||||
ACTIVE: "启用",
|
||||
REJECTED: "已拒绝",
|
||||
DISABLED: "已禁用",
|
||||
},
|
||||
subjectStatus: {
|
||||
|
||||
@@ -14,7 +14,6 @@ import StudyHome from "../views/StudyHome.vue";
|
||||
import FaqDetail from "../views/FaqDetail.vue";
|
||||
import AuditLogs from "../views/admin/AuditLogs.vue";
|
||||
import AdminUsers from "../views/admin/Users.vue";
|
||||
import AdminUserApproval from "../views/admin/AdminUserApproval.vue";
|
||||
import AdminProjects from "../views/admin/Projects.vue";
|
||||
import AdminSites from "../views/admin/Sites.vue";
|
||||
import PermissionManagement from "../views/admin/PermissionManagement.vue";
|
||||
@@ -334,12 +333,6 @@ const routes: RouteRecordRaw[] = [
|
||||
component: AdminUsers,
|
||||
meta: { title: TEXT.menu.accountManagement, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "user-approval",
|
||||
name: "AdminUserApproval",
|
||||
component: AdminUserApproval,
|
||||
meta: { title: TEXT.modules.adminUserApproval.title, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: "projects",
|
||||
name: "AdminProjects",
|
||||
|
||||
@@ -300,7 +300,7 @@ const onSubmit = async () => {
|
||||
} else if (status === 429) {
|
||||
loginError.value = { title: "操作过于频繁", message: "登录尝试次数过多,请稍后再试" };
|
||||
} else if (status === 403 || detail.includes("未激活") || detail.includes("审核") || detail.includes("pending")) {
|
||||
loginError.value = { title: "账号待审核", message: "您的账号尚未通过管理员审批,请耐心等待或联系管理员" };
|
||||
loginError.value = { title: "账号不可用", message: "请联系系统管理员确认账号状态" };
|
||||
} else if (detail.includes("锁定") || detail.includes("locked") || detail.includes("disabled")) {
|
||||
loginError.value = { title: "账号已被锁定", message: "请联系系统管理员解锁账号" };
|
||||
} else if (status === 401) {
|
||||
|
||||
@@ -109,9 +109,9 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="approval-title">注册申请已提交</h3>
|
||||
<p class="approval-message">待管理员审核通过后方可登录系统。</p>
|
||||
<p class="approval-hint">管理员审核通过前,请勿重复提交注册申请。</p>
|
||||
<h3 class="approval-title">注册成功</h3>
|
||||
<p class="approval-message">账号已启用,可以立即登录系统。</p>
|
||||
<p class="approval-hint">请使用刚注册的邮箱和密码登录。</p>
|
||||
<RouterLink to="/login" class="back-to-login-btn">返回登录</RouterLink>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
<template>
|
||||
<div class="page page--flush">
|
||||
<div class="main-content-flat unified-shell">
|
||||
<!-- 顶部状态切换 -->
|
||||
<div class="approval-header unified-action-bar">
|
||||
<div class="filter-form">
|
||||
<div class="status-tabs">
|
||||
<button
|
||||
v-for="s in statusOptions"
|
||||
:key="s.value"
|
||||
class="status-tab"
|
||||
:class="{ active: status === s.value }"
|
||||
@click="status = s.value; loadUsers()"
|
||||
>
|
||||
<span class="tab-dot" :class="'dot--' + s.value.toLowerCase()"></span>
|
||||
{{ s.label }}
|
||||
<span v-if="s.value === 'PENDING' && pendingCount > 0" class="tab-badge">{{ pendingCount }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="approval-content">
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && users.length === 0" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="empty-title">{{ status === 'PENDING' ? '暂无待审批用户' : '暂无数据' }}</p>
|
||||
<p class="empty-desc">{{ status === 'PENDING' ? '所有注册申请已处理完毕' : '当前筛选条件下没有用户' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 用户卡片列表 -->
|
||||
<div v-else class="user-cards" v-loading="loading">
|
||||
<div v-for="user in users" :key="user.id" class="user-card" :class="'card--' + (user.status || '').toLowerCase()">
|
||||
<div class="card-main">
|
||||
<div class="card-avatar" :class="'avatar--' + (user.status || '').toLowerCase()">
|
||||
{{ getInitials(user.full_name) }}
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name">{{ user.full_name }}</div>
|
||||
<div class="card-email">{{ user.email }}</div>
|
||||
<div class="card-meta">
|
||||
<span class="meta-item" v-if="user.clinical_department">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12">
|
||||
<path d="M19 21V5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-1 4h1"/>
|
||||
</svg>
|
||||
{{ user.clinical_department }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>
|
||||
{{ displayDateTime(user.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<template v-if="user.status === 'PENDING'">
|
||||
<el-button type="success" @click="onApprove(user.id)" class="action-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
{{ TEXT.modules.adminUserApproval.approve }}
|
||||
</el-button>
|
||||
<el-button type="danger" plain @click="onReject(user.id)" class="action-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
{{ TEXT.modules.adminUserApproval.reject }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag :type="statusType(user.status)" effect="light" class="status-tag">
|
||||
{{ displayEnum(TEXT.enums.userStatus, user.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { approveUser, listPendingUsers, rejectUser } from "../../api/admin";
|
||||
import type { UserInfo, UserStatus } from "../../types/api";
|
||||
import { displayDateTime, displayEnum } from "../../utils/display";
|
||||
import { TEXT } from "../../locales";
|
||||
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const status = ref<UserStatus>("PENDING");
|
||||
const pendingCount = ref(0);
|
||||
|
||||
const statusOptions = [
|
||||
{ value: "PENDING" as UserStatus, label: TEXT.enums.userStatus.PENDING },
|
||||
{ value: "ACTIVE" as UserStatus, label: TEXT.enums.userStatus.ACTIVE },
|
||||
{ value: "REJECTED" as UserStatus, label: TEXT.enums.userStatus.REJECTED },
|
||||
{ value: "DISABLED" as UserStatus, label: TEXT.enums.userStatus.DISABLED },
|
||||
];
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
if (!name) return '?';
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
return name.slice(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
const statusType = (s: UserStatus) => {
|
||||
switch (s) {
|
||||
case "ACTIVE": return "success";
|
||||
case "PENDING": return "warning";
|
||||
case "REJECTED": return "danger";
|
||||
default: return "info";
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await listPendingUsers(status.value);
|
||||
users.value = data.items || [];
|
||||
if (status.value === "PENDING") {
|
||||
pendingCount.value = users.value.length;
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.loadFailed);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPendingCount = async () => {
|
||||
try {
|
||||
const { data } = await listPendingUsers("PENDING");
|
||||
pendingCount.value = (data.items || []).length;
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
};
|
||||
|
||||
const onApprove = async (id: string) => {
|
||||
try {
|
||||
await approveUser(id);
|
||||
ElMessage.success(TEXT.modules.adminUserApproval.approveSuccess);
|
||||
loadUsers();
|
||||
loadPendingCount();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.approveFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const onReject = async (id: string) => {
|
||||
try {
|
||||
await rejectUser(id);
|
||||
ElMessage.success(TEXT.modules.adminUserApproval.rejectSuccess);
|
||||
loadUsers();
|
||||
loadPendingCount();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.rejectFailed);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers();
|
||||
if (status.value !== "PENDING") {
|
||||
loadPendingCount();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.approval-header {
|
||||
border-bottom: 1px solid var(--unified-shell-divider);
|
||||
}
|
||||
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: var(--ctms-transition);
|
||||
}
|
||||
|
||||
.status-tab:hover {
|
||||
background: var(--ctms-neutral-100);
|
||||
color: var(--ctms-text-main);
|
||||
}
|
||||
|
||||
.status-tab.active {
|
||||
background: var(--ctms-primary-light);
|
||||
color: var(--ctms-primary);
|
||||
}
|
||||
|
||||
.tab-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.tab-dot.dot--pending { color: var(--ctms-warning); }
|
||||
.tab-dot.dot--active { color: var(--ctms-success); }
|
||||
.tab-dot.dot--rejected { color: var(--ctms-danger); }
|
||||
.tab-dot.dot--disabled { color: var(--ctms-text-disabled); }
|
||||
|
||||
.tab-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: var(--ctms-danger);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.approval-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
background: #e6f4ed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-icon svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--ctms-success);
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-main);
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 13px;
|
||||
color: var(--ctms-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 用户卡片 */
|
||||
.user-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--ctms-border-color);
|
||||
background: #fff;
|
||||
transition: var(--ctms-transition);
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
border-color: var(--ctms-border-color-hover);
|
||||
box-shadow: var(--ctms-shadow-sm);
|
||||
}
|
||||
|
||||
.user-card.card--pending {
|
||||
border-left: 3px solid var(--ctms-warning);
|
||||
}
|
||||
|
||||
.card-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
background: var(--ctms-primary);
|
||||
}
|
||||
|
||||
.card-avatar.avatar--active { background: linear-gradient(135deg, #3f8f6b, #2d7a5a); }
|
||||
.card-avatar.avatar--pending { background: linear-gradient(135deg, #c58b2a, #a87420); }
|
||||
.card-avatar.avatar--disabled { background: linear-gradient(135deg, #94a3b8, #64748b); }
|
||||
.card-avatar.avatar--rejected { background: linear-gradient(135deg, #c24b4b, #a83a3a); }
|
||||
|
||||
.card-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ctms-text-main);
|
||||
}
|
||||
|
||||
.card-email {
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--ctms-text-secondary);
|
||||
}
|
||||
|
||||
.meta-item svg {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.user-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.card-actions {
|
||||
margin-left: 56px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -29,18 +29,6 @@
|
||||
<span class="stat-label">{{ TEXT.enums.userStatus.ACTIVE }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card--pending">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-body">
|
||||
<span class="stat-value">{{ pendingCount }}</span>
|
||||
<span class="stat-label">{{ TEXT.enums.userStatus.PENDING }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card--disabled">
|
||||
<div class="stat-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
@@ -70,9 +58,7 @@
|
||||
</div>
|
||||
<div class="filter-item-form">
|
||||
<el-select v-model="statusFilter" :placeholder="TEXT.common.fields.status" style="width: 140px" clearable class="filter-select-comp" @change="applyFilters">
|
||||
<el-option :label="TEXT.enums.userStatus.PENDING" value="PENDING" />
|
||||
<el-option :label="TEXT.enums.userStatus.ACTIVE" value="ACTIVE" />
|
||||
<el-option :label="TEXT.enums.userStatus.REJECTED" value="REJECTED" />
|
||||
<el-option :label="TEXT.enums.userStatus.DISABLED" value="DISABLED" />
|
||||
</el-select>
|
||||
</div>
|
||||
@@ -189,23 +175,11 @@ const auth = useAuthStore();
|
||||
let keywordSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const activeCount = computed(() => allUsers.value.filter(u => u.status === 'ACTIVE').length);
|
||||
const pendingCount = computed(() => allUsers.value.filter(u => u.status === 'PENDING').length);
|
||||
const disabledCount = computed(() => allUsers.value.filter(u => u.status === 'DISABLED').length);
|
||||
|
||||
const statusType = (status: string) => {
|
||||
switch (status) {
|
||||
case "ACTIVE": return "success";
|
||||
case "PENDING": return "warning";
|
||||
case "REJECTED": return "danger";
|
||||
default: return "info";
|
||||
}
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case "ACTIVE": return TEXT.enums.userStatus.ACTIVE;
|
||||
case "PENDING": return TEXT.enums.userStatus.PENDING;
|
||||
case "REJECTED": return TEXT.enums.userStatus.REJECTED;
|
||||
case "DISABLED": return TEXT.enums.userStatus.DISABLED;
|
||||
default: return status || TEXT.common.fallback;
|
||||
}
|
||||
@@ -378,7 +352,7 @@ onBeforeUnmount(() => {
|
||||
<style scoped>
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--unified-shell-divider);
|
||||
@@ -424,11 +398,6 @@ onBeforeUnmount(() => {
|
||||
color: var(--ctms-success);
|
||||
}
|
||||
|
||||
.stat-card--pending .stat-icon {
|
||||
background: #fef3e2;
|
||||
color: var(--ctms-warning);
|
||||
}
|
||||
|
||||
.stat-card--disabled .stat-icon {
|
||||
background: #fde8e8;
|
||||
color: var(--ctms-danger);
|
||||
@@ -517,21 +486,11 @@ onBeforeUnmount(() => {
|
||||
box-shadow: 0 0 0 3px rgba(63, 143, 107, 0.15);
|
||||
}
|
||||
|
||||
.status-dot.dot--pending {
|
||||
background: var(--ctms-warning);
|
||||
box-shadow: 0 0 0 3px rgba(197, 139, 42, 0.15);
|
||||
}
|
||||
|
||||
.status-dot.dot--disabled {
|
||||
background: var(--ctms-text-disabled);
|
||||
box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.15);
|
||||
}
|
||||
|
||||
.status-dot.dot--rejected {
|
||||
background: var(--ctms-danger);
|
||||
box-shadow: 0 0 0 3px rgba(194, 75, 75, 0.15);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--ctms-text-secondary);
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user