管理后台前后端逻辑、UI美化
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>{{ TEXT.modules.adminUsers.title }}</h3>
|
||||
<p class="sub">{{ TEXT.modules.adminUsers.subtitle }}</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminUsers.userLabel }}</el-button>
|
||||
</div>
|
||||
@@ -22,28 +21,32 @@
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.adminUsers.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="320" fixed="right">
|
||||
<el-table-column :label="TEXT.common.labels.actions" min-width="200" align="center">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'ACTIVE' ? 'danger' : 'primary'"
|
||||
size="small"
|
||||
:disabled="isLastAdmin(scope.row)"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.status === 'ACTIVE' ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="openReset(scope.row)">{{ TEXT.modules.adminUsers.resetPassword }}</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="scope.row.id === auth.user?.id"
|
||||
@click="onDelete(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
<el-tooltip :content="TEXT.common.actions.edit" placement="top">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEdit(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="scope.row.status === 'ACTIVE' ? TEXT.common.actions.disable : TEXT.common.actions.enable" placement="top">
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="scope.row.status === 'ACTIVE' ? Lock : Unlock"
|
||||
:disabled="isLastAdmin(scope.row)"
|
||||
@click="toggleStatus(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.modules.adminUsers.resetPassword" placement="top">
|
||||
<el-button link type="warning" :icon="Key" @click="openReset(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.common.actions.delete" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
:disabled="scope.row.id === auth.user?.id"
|
||||
@click="onDelete(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -66,7 +69,10 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Edit, Delete, Key, Lock, Unlock } from "@element-plus/icons-vue";
|
||||
import { fetchUsers, updateUser, deleteUser } from "../../api/users";
|
||||
import { fetchStudies } from "../../api/studies";
|
||||
import { listMembers } from "../../api/members";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
import UserForm from "./UserForm.vue";
|
||||
import UserResetPassword from "./UserResetPassword.vue";
|
||||
@@ -86,6 +92,37 @@ const editingUser = ref<UserInfo | null>(null);
|
||||
const resetUser = ref<UserInfo | null>(null);
|
||||
const auth = useAuthStore();
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const isLastAdmin = (row: UserInfo) =>
|
||||
row.role === "ADMIN" && row.status === "ACTIVE" && activeAdminCount.value <= 1;
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -146,67 +183,71 @@ const openReset = (row: UserInfo) => {
|
||||
};
|
||||
|
||||
const onDelete = async (row: UserInfo) => {
|
||||
const { value, action } = await ElMessageBox.prompt(
|
||||
const { action } = await ElMessageBox.prompt(
|
||||
TEXT.modules.adminUsers.deleteConfirm,
|
||||
TEXT.modules.adminUsers.deleteTitle,
|
||||
{
|
||||
inputType: "password",
|
||||
inputPlaceholder: TEXT.modules.adminUsers.deletePasswordPlaceholder,
|
||||
confirmButtonText: TEXT.modules.adminUsers.deleteConfirmButton,
|
||||
inputPlaceholder: TEXT.modules.adminUsers.deleteConfirmPlaceholder,
|
||||
confirmButtonText: TEXT.common.actions.confirm,
|
||||
confirmButtonClass: "el-button--danger",
|
||||
cancelButtonText: TEXT.common.actions.cancel,
|
||||
inputPattern: /^确认删除$/,
|
||||
inputErrorMessage: TEXT.modules.adminUsers.deleteConfirmInputError,
|
||||
}
|
||||
).catch(() => ({ action: "cancel", value: "" }));
|
||||
if (action !== "confirm" || !value) return;
|
||||
).catch(() => ({ action: "cancel" }));
|
||||
if (action !== "confirm") return;
|
||||
try {
|
||||
await deleteUser(row.id, { admin_password: value });
|
||||
await deleteUser(row.id);
|
||||
ElMessage.success(TEXT.modules.adminUsers.deleteSuccess);
|
||||
loadUsers();
|
||||
} catch (e: any) {
|
||||
const detail = e?.response?.data?.detail || e?.response?.data?.message;
|
||||
if (detail && detail.includes(TEXT.modules.adminProjectMembers.memberLabel)) {
|
||||
ElMessage.closeAll();
|
||||
ElMessage.error(detail);
|
||||
const blockingProjects = await findUserProjects(row.id);
|
||||
if (blockingProjects.length > 0) {
|
||||
ElMessage.error(`${detail},该用户仍在以下项目中:${blockingProjects.join("、")}`);
|
||||
} else {
|
||||
ElMessage.error(detail);
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(detail || TEXT.modules.adminUsers.deleteFailed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const findUserProjects = async (userId: string) => {
|
||||
try {
|
||||
const { data } = await fetchStudies();
|
||||
const studies = Array.isArray(data) ? data : (data as any).items || [];
|
||||
const blocking: string[] = [];
|
||||
|
||||
// Check each study for membership
|
||||
// We run these in parallel but limited batch might be better if many studies.
|
||||
// For now assuming reasonable number of studies.
|
||||
await Promise.all(studies.map(async (study: any) => {
|
||||
try {
|
||||
const { data: membersData } = await listMembers(study.id, { limit: 1000, include_inactive: true }); // Large limit to catch all
|
||||
const members = Array.isArray(membersData) ? membersData : (membersData as any).items || [];
|
||||
if (members.some((m: any) => m.user_id === userId)) {
|
||||
blocking.push(study.name);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors checking specific study
|
||||
}
|
||||
}));
|
||||
return blocking;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers();
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const isLastAdmin = (row: UserInfo) =>
|
||||
row.role === "ADMIN" && row.status === "ACTIVE" && activeAdminCount.value <= 1;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user