230 lines
7.1 KiB
Vue
230 lines
7.1 KiB
Vue
<template>
|
||
<div class="page">
|
||
<el-card>
|
||
<div class="header">
|
||
<div>
|
||
<h3>账号治理(系统登录账号)</h3>
|
||
<p class="sub">账号用于登录系统,本身不具备项目或角色权限</p>
|
||
</div>
|
||
<el-button type="primary" @click="openCreate">新建用户</el-button>
|
||
</div>
|
||
<el-table :data="users" stripe v-loading="loading" style="width: 100%">
|
||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||
<el-table-column prop="full_name" label="姓名" min-width="140" />
|
||
<el-table-column prop="department" label="部门" min-width="140" />
|
||
<el-table-column label="状态" width="140">
|
||
<template #default="scope">
|
||
<el-tag :type="statusType(scope.row.status)">
|
||
{{ statusLabel(scope.row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="created_at" label="创建时间" width="200">
|
||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="320" fixed="right">
|
||
<template #default="scope">
|
||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</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' ? "禁用" : "启用" }}
|
||
</el-button>
|
||
<el-button link type="danger" size="small" @click="openReset(scope.row)">重置密码</el-button>
|
||
<el-button
|
||
link
|
||
type="danger"
|
||
size="small"
|
||
:disabled="scope.row.id === auth.user?.id"
|
||
@click="onDelete(scope.row)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pagination">
|
||
<el-pagination
|
||
background
|
||
layout="prev, pager, next, total"
|
||
:page-size="pageSize"
|
||
:total="total"
|
||
v-model:current-page="page"
|
||
@current-change="loadUsers"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
<UserForm v-model:visible="formVisible" :user="editingUser" :admin-count="activeAdminCount" @saved="loadUsers" />
|
||
<UserResetPassword v-model:visible="resetVisible" :user="resetUser" @reset="loadUsers" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, ref } from "vue";
|
||
import { ElMessage, ElMessageBox } from "element-plus";
|
||
import { fetchUsers, updateUser, deleteUser } from "../../api/users";
|
||
import type { UserInfo } from "../../types/api";
|
||
import UserForm from "./UserForm.vue";
|
||
import UserResetPassword from "./UserResetPassword.vue";
|
||
import { useAuthStore } from "../../store/auth";
|
||
import { displayDateTime } from "../../utils/display";
|
||
|
||
const users = ref<UserInfo[]>([]);
|
||
const total = ref(0);
|
||
const page = ref(1);
|
||
const pageSize = ref(10);
|
||
const loading = ref(false);
|
||
const activeAdminCount = ref(0);
|
||
const formVisible = ref(false);
|
||
const resetVisible = ref(false);
|
||
const editingUser = ref<UserInfo | null>(null);
|
||
const resetUser = ref<UserInfo | null>(null);
|
||
const auth = useAuthStore();
|
||
|
||
const loadUsers = async () => {
|
||
loading.value = true;
|
||
try {
|
||
const [{ data }, { data: adminData }] = await Promise.all([
|
||
fetchUsers({ skip: (page.value - 1) * pageSize.value, limit: pageSize.value }),
|
||
fetchUsers({ skip: 0, limit: 10000 }),
|
||
]);
|
||
const items = (data as any).items || [];
|
||
const allItems = (adminData as any).items || [];
|
||
users.value = items;
|
||
total.value = (data as any).total || items.length;
|
||
activeAdminCount.value = allItems.filter((u: UserInfo) => u.role === "ADMIN" && u.status === "ACTIVE").length;
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message || "用户列表加载失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const openCreate = () => {
|
||
editingUser.value = null;
|
||
formVisible.value = true;
|
||
};
|
||
|
||
const openEdit = (row: UserInfo) => {
|
||
editingUser.value = row;
|
||
formVisible.value = true;
|
||
};
|
||
|
||
const toggleStatus = async (row: UserInfo) => {
|
||
if (isLastAdmin(row)) {
|
||
ElMessage.warning("至少保留一个管理员账号,不能禁用该账号");
|
||
return;
|
||
}
|
||
const disable = row.status === "ACTIVE";
|
||
const ok = await ElMessageBox.confirm(
|
||
disable ? "该操作将影响用户账号,请确认是否继续" : "确认启用该用户账号?",
|
||
disable ? "禁用账号" : "启用账号",
|
||
{
|
||
type: disable ? "warning" : "info",
|
||
confirmButtonText: "确认",
|
||
cancelButtonText: "取消",
|
||
}
|
||
).catch(() => null);
|
||
if (!ok) return;
|
||
try {
|
||
await updateUser(row.id, { status: disable ? "DISABLED" : "ACTIVE", is_active: !disable });
|
||
ElMessage.success(disable ? "已禁用" : "已启用");
|
||
loadUsers();
|
||
} catch (e: any) {
|
||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||
}
|
||
};
|
||
|
||
const openReset = (row: UserInfo) => {
|
||
resetUser.value = row;
|
||
resetVisible.value = true;
|
||
};
|
||
|
||
const onDelete = async (row: UserInfo) => {
|
||
const { value, action } = await ElMessageBox.prompt(
|
||
"删除后账号将无法登录且无法恢复,需先在各项目成员配置中移除该账号。请输入当前管理员密码确认删除。",
|
||
"危险操作:删除账号",
|
||
{
|
||
inputType: "password",
|
||
inputPlaceholder: "请输入 admin 密码",
|
||
confirmButtonText: "确认删除",
|
||
confirmButtonClass: "el-button--danger",
|
||
cancelButtonText: "取消",
|
||
}
|
||
).catch(() => ({ action: "cancel", value: "" }));
|
||
if (action !== "confirm" || !value) return;
|
||
try {
|
||
await deleteUser(row.id, { admin_password: value });
|
||
ElMessage.success("账号已删除");
|
||
loadUsers();
|
||
} catch (e: any) {
|
||
const detail = e?.response?.data?.detail || e?.response?.data?.message;
|
||
if (detail && detail.includes("项目成员")) {
|
||
ElMessage.closeAll();
|
||
ElMessage.error(detail);
|
||
} else {
|
||
ElMessage.error(detail || "删除失败");
|
||
}
|
||
}
|
||
};
|
||
|
||
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 "启用";
|
||
case "PENDING":
|
||
return "待审核";
|
||
case "REJECTED":
|
||
return "已拒绝";
|
||
case "DISABLED":
|
||
return "已停用";
|
||
default:
|
||
return status || "未知";
|
||
}
|
||
};
|
||
|
||
const isLastAdmin = (row: UserInfo) =>
|
||
row.role === "ADMIN" && row.status === "ACTIVE" && activeAdminCount.value <= 1;
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
padding: 16px;
|
||
}
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
.sub {
|
||
color: #666;
|
||
margin-top: 4px;
|
||
}
|
||
.pagination {
|
||
margin-top: 12px;
|
||
text-align: right;
|
||
}
|
||
</style>
|