帐号治理-帐号管理--更新
This commit is contained in:
@@ -4,8 +4,10 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.deps import get_db_session, require_roles
|
from app.core.deps import get_db_session, require_roles
|
||||||
|
from app.core.security import verify_password
|
||||||
from app.schemas.common import PaginatedResponse
|
from app.schemas.common import PaginatedResponse
|
||||||
from app.crud import user as user_crud
|
from app.crud import user as user_crud
|
||||||
|
from app.crud import member as member_crud
|
||||||
from app.utils.pagination import paginate
|
from app.utils.pagination import paginate
|
||||||
from app.schemas.user import UserCreate, UserRead, UserUpdate
|
from app.schemas.user import UserCreate, UserRead, UserUpdate
|
||||||
|
|
||||||
@@ -52,3 +54,27 @@ async def update_user(
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
user = await user_crud.update_user(db, db_user, user_in)
|
user = await user_crud.update_user(db, db_user, user_in)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_user(
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
payload: dict,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(require_roles(["ADMIN"])),
|
||||||
|
):
|
||||||
|
admin_password = payload.get("admin_password")
|
||||||
|
if not admin_password or not verify_password(admin_password, current_user.hashed_password):
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="管理员密码错误")
|
||||||
|
db_user = await user_crud.get_by_id(db, user_id)
|
||||||
|
if not db_user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
|
if db_user.id == current_user.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不允许删除自己")
|
||||||
|
has_membership = await member_crud.user_has_memberships(db, db_user.id)
|
||||||
|
if has_membership:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="该账号仍在项目成员中,请先在项目成员配置中移除后再删除",
|
||||||
|
)
|
||||||
|
await user_crud.delete_user(db, db_user)
|
||||||
|
|||||||
@@ -62,3 +62,8 @@ async def remove_member(db: AsyncSession, member: StudyMember) -> StudyMember:
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(member)
|
await db.refresh(member)
|
||||||
return member
|
return member
|
||||||
|
|
||||||
|
|
||||||
|
async def user_has_memberships(db: AsyncSession, user_id: uuid.UUID) -> bool:
|
||||||
|
result = await db.execute(select(StudyMember.id).where(StudyMember.user_id == user_id))
|
||||||
|
return result.first() is not None
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import select, update
|
from sqlalchemy import select, update, delete
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.security import hash_password
|
from app.core.security import hash_password
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.user import UserCreate, UserUpdate
|
from app.schemas.user import UserCreate, UserUpdate
|
||||||
|
from app.models.study_member import StudyMember
|
||||||
|
|
||||||
|
|
||||||
async def get_by_username(db: AsyncSession, username: str) -> User | None:
|
async def get_by_username(db: AsyncSession, username: str) -> User | None:
|
||||||
@@ -70,3 +71,9 @@ async def ensure_admin_exists(db: AsyncSession, *, default_password: str = "admi
|
|||||||
)
|
)
|
||||||
db.add(new_admin)
|
db.add(new_admin)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_user(db: AsyncSession, user: User) -> None:
|
||||||
|
await db.execute(delete(StudyMember).where(StudyMember.user_id == user.id))
|
||||||
|
await db.delete(user)
|
||||||
|
await db.commit()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPatch, apiPost } from "./axios";
|
import { apiGet, apiPatch, apiPost } from "./axios";
|
||||||
import type { ApiListResponse, UserInfo } from "../types/api";
|
import type { ApiListResponse, UserInfo } from "../types/api";
|
||||||
|
import { apiDelete } from "./axios";
|
||||||
|
|
||||||
export const fetchUsers = (params?: Record<string, any>) =>
|
export const fetchUsers = (params?: Record<string, any>) =>
|
||||||
apiGet<ApiListResponse<UserInfo>>("/api/v1/users", { params });
|
apiGet<ApiListResponse<UserInfo>>("/api/v1/users", { params });
|
||||||
@@ -9,3 +10,6 @@ export const createUser = (payload: { username: string; password: string; role:
|
|||||||
|
|
||||||
export const updateUser = (userId: string, payload: Partial<{ role: string; is_active: boolean; password: string }>) =>
|
export const updateUser = (userId: string, payload: Partial<{ role: string; is_active: boolean; password: string }>) =>
|
||||||
apiPatch<UserInfo>(`/api/v1/users/${userId}`, payload);
|
apiPatch<UserInfo>(`/api/v1/users/${userId}`, payload);
|
||||||
|
|
||||||
|
export const deleteUser = (userId: string, payload: { admin_password: string }) =>
|
||||||
|
apiDelete<void>(`/api/v1/users/${userId}`, { data: payload });
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="added_at" label="加入时间" width="200" />
|
<el-table-column prop="added_at" label="加入时间" width="200" />
|
||||||
<el-table-column label="操作" width="260" fixed="right">
|
<el-table-column label="操作" width="320" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="scope.row.role_in_study"
|
v-model="scope.row.role_in_study"
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
<el-option label="IMP" value="IMP" />
|
<el-option label="IMP" value="IMP" />
|
||||||
<el-option label="ADMIN" value="ADMIN" />
|
<el-option label="ADMIN" value="ADMIN" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-button link type="danger" size="small" @click="onDelete(scope.row)">删除</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||||
@@ -271,7 +272,7 @@ const toggleActive = async (row: StudyMember) => {
|
|||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await removeMember(projectId.value, row.id);
|
await updateMember(projectId.value, row.id, { is_active: false });
|
||||||
ElMessage.success("成员已停用");
|
ElMessage.success("成员已停用");
|
||||||
loadMembers();
|
loadMembers();
|
||||||
logAudit("PROJECT_MEMBER_UPDATED", {
|
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||||
@@ -318,6 +319,53 @@ const toggleActive = async (row: StudyMember) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDelete = async (row: StudyMember) => {
|
||||||
|
if (!projectId.value) return;
|
||||||
|
const decision = evaluateAction({
|
||||||
|
actorRole: auth.user?.role || null,
|
||||||
|
requiredPermission: "project.members.manage",
|
||||||
|
target: { projectId: projectId.value, memberId: row.id },
|
||||||
|
});
|
||||||
|
if (!decision.allowed) {
|
||||||
|
ElMessage.warning(decision.reason || "无权限执行该操作");
|
||||||
|
logAudit(decision.auditType, {
|
||||||
|
targetId: projectId.value,
|
||||||
|
targetName: project.value?.name,
|
||||||
|
reason: decision.reason,
|
||||||
|
severity: decision.severity,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = await ElMessageBox.confirm("确认将该账号从本项目移除?账号本身不会被删除。", "删除成员", {
|
||||||
|
type: "warning",
|
||||||
|
confirmButtonText: "确认",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
}).catch(() => null);
|
||||||
|
if (!ok) return;
|
||||||
|
try {
|
||||||
|
await removeMember(projectId.value, row.id);
|
||||||
|
members.value = members.value.filter((m) => m.id !== row.id);
|
||||||
|
ElMessage.success("已从项目移除");
|
||||||
|
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||||
|
targetId: projectId.value,
|
||||||
|
targetName: project.value?.name,
|
||||||
|
before: { is_active: row.is_active },
|
||||||
|
after: { removed_member_id: row.id },
|
||||||
|
severity: "normal",
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||||
|
logAudit("PROJECT_MEMBER_UPDATED", {
|
||||||
|
targetId: projectId.value,
|
||||||
|
targetName: project.value?.name,
|
||||||
|
before: { is_active: row.is_active },
|
||||||
|
after: { removed_member_id: row.id },
|
||||||
|
severity: "warning",
|
||||||
|
reason: e?.response?.data?.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const availableUsers = computed(() => users.value);
|
const availableUsers = computed(() => users.value);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -10,8 +10,23 @@
|
|||||||
<el-form-item label="PI" prop="pi_name">
|
<el-form-item label="PI" prop="pi_name">
|
||||||
<el-input v-model="form.pi_name" placeholder="PI(可选)" />
|
<el-input v-model="form.pi_name" placeholder="PI(可选)" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="CRA 联系人" prop="contact">
|
<el-form-item label="电话" prop="phone">
|
||||||
<el-input v-model="form.contact" placeholder="多个联系人用逗号分隔" />
|
<el-input v-model="form.phone" placeholder="电话(可选)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="负责人" prop="contact">
|
||||||
|
<el-select
|
||||||
|
v-model="form.craSelections"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
|
placeholder="选择项目成员作为负责人,可多选"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="user in craOptions"
|
||||||
|
:key="user.value"
|
||||||
|
:label="user.label"
|
||||||
|
:value="user.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="site" label="状态" prop="is_active">
|
<el-form-item v-if="site" label="状态" prop="is_active">
|
||||||
<el-switch v-model="form.is_active" active-text="启用" inactive-text="停用" />
|
<el-switch v-model="form.is_active" active-text="启用" inactive-text="停用" />
|
||||||
@@ -37,6 +52,8 @@ const props = defineProps<{
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
studyId: string;
|
studyId: string;
|
||||||
site?: Site | null;
|
site?: Site | null;
|
||||||
|
members?: any[];
|
||||||
|
users?: any[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -56,7 +73,9 @@ const form = reactive({
|
|||||||
name: "",
|
name: "",
|
||||||
city: "",
|
city: "",
|
||||||
pi_name: "",
|
pi_name: "",
|
||||||
|
phone: "",
|
||||||
contact: "",
|
contact: "",
|
||||||
|
craSelections: [] as string[],
|
||||||
is_active: true,
|
is_active: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,11 +83,24 @@ const rules = reactive<FormRules>({
|
|||||||
name: [{ required: true, message: "请输入中心名称", trigger: "blur" }],
|
name: [{ required: true, message: "请输入中心名称", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const craOptions = computed(() => {
|
||||||
|
const userMap = (props.users || []).reduce<Record<string, string>>((acc, cur: any) => {
|
||||||
|
if (cur?.id) acc[cur.id] = cur.username || cur.id;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
return (props.members || []).map((m: any) => ({
|
||||||
|
label: userMap[m.user_id] || m.username || m.user_id,
|
||||||
|
value: m.user_id,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
form.name = "";
|
form.name = "";
|
||||||
form.city = "";
|
form.city = "";
|
||||||
form.pi_name = "";
|
form.pi_name = "";
|
||||||
|
form.phone = "";
|
||||||
form.contact = "";
|
form.contact = "";
|
||||||
|
form.craSelections = [];
|
||||||
form.is_active = true;
|
form.is_active = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +113,17 @@ watch(
|
|||||||
form.name = props.site.name;
|
form.name = props.site.name;
|
||||||
form.city = props.site.city || "";
|
form.city = props.site.city || "";
|
||||||
form.pi_name = props.site.pi_name || "";
|
form.pi_name = props.site.pi_name || "";
|
||||||
|
form.phone = (props.site as any)?.contact_phone || (props.site as any)?.phone || "";
|
||||||
form.contact = props.site.contact || "";
|
form.contact = props.site.contact || "";
|
||||||
|
const rawSelections = (props.site.contact || "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const optionByLabel: Record<string, string> = {};
|
||||||
|
craOptions.value.forEach((opt) => {
|
||||||
|
optionByLabel[opt.label] = opt.value;
|
||||||
|
});
|
||||||
|
form.craSelections = rawSelections.map((s) => optionByLabel[s] || s);
|
||||||
form.is_active = props.site.is_active;
|
form.is_active = props.site.is_active;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,12 +150,13 @@ const onSubmit = async () => {
|
|||||||
await formRef.value.validate();
|
await formRef.value.validate();
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
|
const contact = form.craSelections.length ? form.craSelections.join(",") : form.contact;
|
||||||
if (props.site) {
|
if (props.site) {
|
||||||
await updateSite(props.studyId, props.site.id, {
|
await updateSite(props.studyId, props.site.id, {
|
||||||
name: form.name,
|
name: form.name,
|
||||||
city: form.city,
|
city: form.city,
|
||||||
pi_name: form.pi_name,
|
pi_name: form.pi_name,
|
||||||
contact: form.contact,
|
contact,
|
||||||
is_active: form.is_active,
|
is_active: form.is_active,
|
||||||
});
|
});
|
||||||
ElMessage.success("中心已更新");
|
ElMessage.success("中心已更新");
|
||||||
@@ -128,7 +171,7 @@ const onSubmit = async () => {
|
|||||||
name: form.name,
|
name: form.name,
|
||||||
city: form.city,
|
city: form.city,
|
||||||
pi_name: form.pi_name,
|
pi_name: form.pi_name,
|
||||||
contact: form.contact,
|
contact,
|
||||||
});
|
});
|
||||||
ElMessage.success("中心已创建");
|
ElMessage.success("中心已创建");
|
||||||
logAudit("SITE_STATUS_CHANGED", {
|
logAudit("SITE_STATUS_CHANGED", {
|
||||||
|
|||||||
@@ -12,17 +12,25 @@
|
|||||||
<el-table-column prop="name" label="中心名称" min-width="180" />
|
<el-table-column prop="name" label="中心名称" min-width="180" />
|
||||||
<el-table-column prop="city" label="城市" width="140" />
|
<el-table-column prop="city" label="城市" width="140" />
|
||||||
<el-table-column prop="pi_name" label="PI" width="160" />
|
<el-table-column prop="pi_name" label="PI" width="160" />
|
||||||
<el-table-column prop="contact" label="CRA 联系人" min-width="180" />
|
<el-table-column label="电话" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ phoneText(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="contact" label="负责人" min-width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ contactLabel(scope.row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="120">
|
<el-table-column label="状态" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag type="success" v-if="scope.row.is_active">启用</el-tag>
|
<el-tag type="success" v-if="scope.row.is_active">启用</el-tag>
|
||||||
<el-tag type="danger" v-else>停用</el-tag>
|
<el-tag type="danger" v-else>停用</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="280" fixed="right">
|
<el-table-column label="操作" width="220" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||||
<el-button link type="primary" size="small" @click="openBind(scope.row)">绑定 CRA</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||||
@@ -40,14 +48,8 @@
|
|||||||
v-model:visible="formVisible"
|
v-model:visible="formVisible"
|
||||||
:study-id="projectId"
|
:study-id="projectId"
|
||||||
:site="editingSite"
|
:site="editingSite"
|
||||||
@saved="loadSites"
|
:members="members"
|
||||||
/>
|
:users="users"
|
||||||
<SiteCraBinding
|
|
||||||
v-if="projectId"
|
|
||||||
v-model:visible="bindVisible"
|
|
||||||
:study-id="projectId"
|
|
||||||
:site="bindingSite"
|
|
||||||
:cra-users="craUsers"
|
|
||||||
@saved="loadSites"
|
@saved="loadSites"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,8 +62,8 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
|||||||
import { fetchStudyDetail } from "../../api/studies";
|
import { fetchStudyDetail } from "../../api/studies";
|
||||||
import { fetchSites, updateSite } from "../../api/sites";
|
import { fetchSites, updateSite } from "../../api/sites";
|
||||||
import { fetchUsers } from "../../api/users";
|
import { fetchUsers } from "../../api/users";
|
||||||
|
import { listMembers } from "../../api/members";
|
||||||
import SiteForm from "./SiteForm.vue";
|
import SiteForm from "./SiteForm.vue";
|
||||||
import SiteCraBinding from "./SiteCraBinding.vue";
|
|
||||||
import type { Site, Study, UserInfo } from "../../types/api";
|
import type { Site, Study, UserInfo } from "../../types/api";
|
||||||
import { useAuthStore } from "../../store/auth";
|
import { useAuthStore } from "../../store/auth";
|
||||||
import { evaluateAction } from "../../guards/actionGuard";
|
import { evaluateAction } from "../../guards/actionGuard";
|
||||||
@@ -74,11 +76,10 @@ const auth = useAuthStore();
|
|||||||
const project = ref<Study | null>(null);
|
const project = ref<Study | null>(null);
|
||||||
const sites = ref<Site[]>([]);
|
const sites = ref<Site[]>([]);
|
||||||
const users = ref<UserInfo[]>([]);
|
const users = ref<UserInfo[]>([]);
|
||||||
|
const members = ref<any[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const formVisible = ref(false);
|
const formVisible = ref(false);
|
||||||
const editingSite = ref<Site | null>(null);
|
const editingSite = ref<Site | null>(null);
|
||||||
const bindVisible = ref(false);
|
|
||||||
const bindingSite = ref<Site | null>(null);
|
|
||||||
|
|
||||||
const loadProject = async () => {
|
const loadProject = async () => {
|
||||||
if (!projectId.value) return;
|
if (!projectId.value) return;
|
||||||
@@ -112,23 +113,53 @@ const loadUsers = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const craUsers = computed(() => users.value.filter((u) => u.role === "CRA" && u.is_active));
|
const loadMembers = async () => {
|
||||||
|
if (!projectId.value) return;
|
||||||
|
try {
|
||||||
|
const { data } = await listMembers(projectId.value, { limit: 500 });
|
||||||
|
members.value = Array.isArray(data) ? data : data.items || [];
|
||||||
|
} catch {
|
||||||
|
members.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const memberNameMap = computed(() => {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
users.value.forEach((u) => {
|
||||||
|
map[u.id] = u.username || u.id;
|
||||||
|
});
|
||||||
|
members.value.forEach((m) => {
|
||||||
|
if (m.user_id && !map[m.user_id]) map[m.user_id] = m.username || m.user_id;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
const contactLabel = (row: any) => {
|
||||||
|
if (!row?.contact) return "—";
|
||||||
|
return String(row.contact)
|
||||||
|
.split(",")
|
||||||
|
.map((c) => c.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((c) => memberNameMap.value[c] || c)
|
||||||
|
.join("、") || "—";
|
||||||
|
};
|
||||||
|
|
||||||
|
const phoneText = (row: any) => row?.phone || row?.contact_phone || "—";
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
|
loadUsers();
|
||||||
|
loadMembers();
|
||||||
editingSite.value = null;
|
editingSite.value = null;
|
||||||
formVisible.value = true;
|
formVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (row: Site) => {
|
const openEdit = (row: Site) => {
|
||||||
|
loadUsers();
|
||||||
|
loadMembers();
|
||||||
editingSite.value = row;
|
editingSite.value = row;
|
||||||
formVisible.value = true;
|
formVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openBind = (row: Site) => {
|
|
||||||
bindingSite.value = row;
|
|
||||||
bindVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSite = async (row: Site) => {
|
const toggleSite = async (row: Site) => {
|
||||||
if (!projectId.value) return;
|
if (!projectId.value) return;
|
||||||
const decision = evaluateAction({
|
const decision = evaluateAction({
|
||||||
@@ -178,7 +209,7 @@ const toggleSite = async (row: Site) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadProject(), loadUsers()]);
|
await Promise.all([loadProject(), loadUsers(), loadMembers()]);
|
||||||
loadSites();
|
loadSites();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="created_at" label="创建时间" width="200" />
|
<el-table-column prop="created_at" label="创建时间" width="200" />
|
||||||
<el-table-column label="操作" width="240" fixed="right">
|
<el-table-column label="操作" width="320" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -29,6 +29,15 @@
|
|||||||
{{ scope.row.is_active ? "禁用" : "启用" }}
|
{{ scope.row.is_active ? "禁用" : "启用" }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button link type="danger" size="small" @click="openReset(scope.row)">重置密码</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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -51,10 +60,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { fetchUsers, updateUser } from "../../api/users";
|
import { fetchUsers, updateUser, deleteUser } from "../../api/users";
|
||||||
import type { UserInfo } from "../../types/api";
|
import type { UserInfo } from "../../types/api";
|
||||||
import UserForm from "./UserForm.vue";
|
import UserForm from "./UserForm.vue";
|
||||||
import UserResetPassword from "./UserResetPassword.vue";
|
import UserResetPassword from "./UserResetPassword.vue";
|
||||||
|
import { useAuthStore } from "../../store/auth";
|
||||||
|
|
||||||
const users = ref<UserInfo[]>([]);
|
const users = ref<UserInfo[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -65,6 +75,7 @@ const formVisible = ref(false);
|
|||||||
const resetVisible = ref(false);
|
const resetVisible = ref(false);
|
||||||
const editingUser = ref<UserInfo | null>(null);
|
const editingUser = ref<UserInfo | null>(null);
|
||||||
const resetUser = ref<UserInfo | null>(null);
|
const resetUser = ref<UserInfo | null>(null);
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -116,6 +127,34 @@ const openReset = (row: UserInfo) => {
|
|||||||
resetVisible.value = true;
|
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(() => {
|
onMounted(() => {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user