管理后台前后端逻辑、UI美化

This commit is contained in:
Cheng Zhou
2026-01-16 13:50:08 +08:00
parent 05c1f9579a
commit 7fdcfdaadd
82 changed files with 3210 additions and 549 deletions
+100 -59
View File
@@ -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>