增加admin管理帐号和项目的界面--初步实现
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<el-card>
|
||||
<h2>欢迎使用 CTMS</h2>
|
||||
<p>当前角色:{{ auth.user?.role || "未知" }}</p>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
onMounted(() => {
|
||||
if (!auth.user) {
|
||||
auth.fetchMe();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<el-dialog :title="project ? '编辑项目' : '新建项目'" width="560px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="项目编号" prop="code">
|
||||
<el-input v-model="form.code" :disabled="!!project" placeholder="请输入项目编号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入项目名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="申办方" prop="sponsor">
|
||||
<el-input v-model="form.sponsor" placeholder="申办方(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="方案号" prop="protocol_no">
|
||||
<el-input v-model="form.protocol_no" placeholder="方案号(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分期" prop="phase">
|
||||
<el-input v-model="form.phase" placeholder="研究分期(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="草稿" value="DRAFT" />
|
||||
<el-option label="进行中" value="ACTIVE" />
|
||||
<el-option label="已关闭" value="CLOSED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { createStudy, updateStudy } from "../../api/studies";
|
||||
import type { Study } from "../../types/api";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
project?: Study | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "saved"): void;
|
||||
}>();
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (val: boolean) => emit("update:visible", val),
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
code: "",
|
||||
name: "",
|
||||
sponsor: "",
|
||||
protocol_no: "",
|
||||
phase: "",
|
||||
status: "DRAFT",
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
code: [{ required: true, message: "请输入项目编号", trigger: "blur" }],
|
||||
name: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择项目状态", trigger: "change" }],
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
form.code = "";
|
||||
form.name = "";
|
||||
form.sponsor = "";
|
||||
form.protocol_no = "";
|
||||
form.phase = "";
|
||||
form.status = "DRAFT";
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.project) {
|
||||
form.code = props.project.code;
|
||||
form.name = props.project.name;
|
||||
form.sponsor = props.project.sponsor || "";
|
||||
form.protocol_no = props.project.protocol_no || "";
|
||||
form.phase = props.project.phase || "";
|
||||
form.status = props.project.status || "DRAFT";
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate();
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (props.project) {
|
||||
await updateStudy(props.project.id, {
|
||||
name: form.name,
|
||||
sponsor: form.sponsor,
|
||||
protocol_no: form.protocol_no,
|
||||
phase: form.phase,
|
||||
status: form.status,
|
||||
});
|
||||
ElMessage.success("项目已更新");
|
||||
} else {
|
||||
await createStudy({
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
sponsor: form.sponsor,
|
||||
protocol_no: form.protocol_no,
|
||||
phase: form.phase,
|
||||
status: form.status,
|
||||
});
|
||||
ElMessage.success("项目已创建");
|
||||
}
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>项目成员配置</h3>
|
||||
<div class="sub" v-if="project">项目:{{ project.code }} - {{ project.name }}</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="openAdd">新增成员</el-button>
|
||||
</div>
|
||||
<el-table :data="memberRows" v-loading="loading" stripe>
|
||||
<el-table-column prop="username" label="用户名" min-width="160" />
|
||||
<el-table-column prop="role_in_study" label="角色" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.role_in_study }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.is_active">启用</el-tag>
|
||||
<el-tag type="danger" v-else>已禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="added_at" label="加入时间" width="200" />
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.role_in_study"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
@change="(val) => updateRole(scope.row.id, val)"
|
||||
:disabled="!scope.row.is_active"
|
||||
>
|
||||
<el-option label="PM" value="PM" />
|
||||
<el-option label="CRA" value="CRA" />
|
||||
</el-select>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||
size="small"
|
||||
@click="toggleActive(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog title="新增成员" width="520px" v-model="addVisible" :close-on-click-modal="false">
|
||||
<el-form :model="newMember" label-width="120px" ref="addFormRef" :rules="addRules">
|
||||
<el-form-item label="用户" prop="user_id">
|
||||
<el-select v-model="newMember.user_id" filterable placeholder="请选择用户">
|
||||
<el-option
|
||||
v-for="user in availableUsers"
|
||||
:key="user.id"
|
||||
:label="`${user.username} (${user.role})`"
|
||||
:value="user.id"
|
||||
:disabled="!user.is_active"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role_in_study">
|
||||
<el-select v-model="newMember.role_in_study" placeholder="请选择角色">
|
||||
<el-option label="PM" value="PM" />
|
||||
<el-option label="CRA" value="CRA" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="addVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="adding" @click="submitAdd">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||
import { fetchUsers } from "../../api/users";
|
||||
import { addMember, listMembers, removeMember, updateMember } from "../../api/members";
|
||||
import { fetchStudyDetail } from "../../api/studies";
|
||||
import type { Study, StudyMember, UserInfo } from "../../types/api";
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = computed(() => route.params.projectId as string);
|
||||
|
||||
const project = ref<Study | null>(null);
|
||||
const members = ref<StudyMember[]>([]);
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const addVisible = ref(false);
|
||||
const adding = ref(false);
|
||||
const addFormRef = ref<FormInstance>();
|
||||
const newMember = reactive({
|
||||
user_id: "",
|
||||
role_in_study: "PM",
|
||||
});
|
||||
|
||||
const addRules = reactive<FormRules>({
|
||||
user_id: [{ required: true, message: "请选择用户", trigger: "change" }],
|
||||
role_in_study: [{ required: true, message: "请选择角色", trigger: "change" }],
|
||||
});
|
||||
|
||||
const loadProject = async () => {
|
||||
if (!projectId.value) return;
|
||||
try {
|
||||
const { data } = await fetchStudyDetail(projectId.value);
|
||||
project.value = data;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const loadMembers = async () => {
|
||||
if (!projectId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await listMembers(projectId.value);
|
||||
members.value = data as any;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "成员列表加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const { data } = await fetchUsers({ limit: 500 });
|
||||
users.value = (data as any).items || [];
|
||||
} catch {
|
||||
users.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const memberRows = computed(() =>
|
||||
members.value.map((m) => {
|
||||
const user = users.value.find((u) => u.id === m.user_id);
|
||||
return {
|
||||
...m,
|
||||
username: user?.username || m.user_id,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const openAdd = () => {
|
||||
newMember.user_id = "";
|
||||
newMember.role_in_study = "PM";
|
||||
addVisible.value = true;
|
||||
};
|
||||
|
||||
const submitAdd = async () => {
|
||||
if (!projectId.value) return;
|
||||
await addFormRef.value?.validate();
|
||||
adding.value = true;
|
||||
try {
|
||||
await addMember(projectId.value, newMember);
|
||||
ElMessage.success("成员已添加");
|
||||
addVisible.value = false;
|
||||
loadMembers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "添加失败");
|
||||
} finally {
|
||||
adding.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateRole = async (memberId: string, role: string) => {
|
||||
if (!projectId.value) return;
|
||||
try {
|
||||
await updateMember(projectId.value, memberId, { role_in_study: role });
|
||||
ElMessage.success("角色已更新");
|
||||
loadMembers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "更新失败");
|
||||
loadMembers();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleActive = async (row: StudyMember) => {
|
||||
if (!projectId.value) return;
|
||||
if (row.is_active) {
|
||||
const ok = await ElMessageBox.confirm("该操作将影响项目成员,请确认是否继续", "停用成员", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
}).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await removeMember(projectId.value, row.id);
|
||||
ElMessage.success("成员已停用");
|
||||
loadMembers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await updateMember(projectId.value, row.id, { is_active: true });
|
||||
ElMessage.success("成员已启用");
|
||||
loadMembers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const availableUsers = computed(() => users.value);
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadProject(), loadUsers()]);
|
||||
loadMembers();
|
||||
});
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<h3>项目管理</h3>
|
||||
<el-button type="primary" @click="openCreate">新建项目</el-button>
|
||||
</div>
|
||||
<el-table :data="projects" v-loading="loading" stripe>
|
||||
<el-table-column prop="code" label="项目编号" width="140" />
|
||||
<el-table-column prop="name" label="项目名称" min-width="200" />
|
||||
<el-table-column prop="sponsor" label="申办方" min-width="160" />
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTag(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="200" />
|
||||
<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="primary" size="small" @click="goMembers(scope.row)">成员配置</el-button>
|
||||
<el-button link type="primary" size="small" @click="goSites(scope.row)">中心管理</el-button>
|
||||
<el-button link type="success" size="small" @click="enterStudy(scope.row)">进入项目</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<ProjectForm v-model:visible="formVisible" :project="editingProject" @saved="loadProjects" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { fetchStudies } from "../../api/studies";
|
||||
import type { Study } from "../../types/api";
|
||||
import ProjectForm from "./ProjectForm.vue";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
|
||||
const projects = ref<Study[]>([]);
|
||||
const loading = ref(false);
|
||||
const formVisible = ref(false);
|
||||
const editingProject = ref<Study | null>(null);
|
||||
const router = useRouter();
|
||||
const studyStore = useStudyStore();
|
||||
|
||||
const loadProjects = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await fetchStudies();
|
||||
const items = (data as any).items || [];
|
||||
projects.value = items;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "项目列表加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
editingProject.value = null;
|
||||
formVisible.value = true;
|
||||
};
|
||||
|
||||
const openEdit = (row: Study) => {
|
||||
editingProject.value = row;
|
||||
formVisible.value = true;
|
||||
};
|
||||
|
||||
const goMembers = (row: Study) => {
|
||||
router.push(`/admin/projects/${row.id}/members`);
|
||||
};
|
||||
|
||||
const goSites = (row: Study) => {
|
||||
router.push(`/admin/projects/${row.id}/sites`);
|
||||
};
|
||||
|
||||
const enterStudy = (row: Study) => {
|
||||
studyStore.setCurrentStudy({ ...row, role_in_study: "PM" } as Study);
|
||||
router.push("/study/home");
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) =>
|
||||
({
|
||||
DRAFT: "草稿",
|
||||
ACTIVE: "进行中",
|
||||
CLOSED: "已关闭",
|
||||
}[status] || status);
|
||||
|
||||
const statusTag = (status: string) =>
|
||||
({
|
||||
DRAFT: "info",
|
||||
ACTIVE: "success",
|
||||
CLOSED: "warning",
|
||||
}[status] || "info");
|
||||
|
||||
onMounted(() => {
|
||||
loadProjects();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<el-dialog title="CRA 绑定" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<div class="tip">中心:{{ site?.name }}</div>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="选择 CRA">
|
||||
<el-select v-model="selectedCras" multiple filterable placeholder="请选择 CRA 账号" style="width: 100%">
|
||||
<el-option v-for="user in craUsers" :key="user.id" :label="user.username" :value="user.id" />
|
||||
</el-select>
|
||||
<div class="hint">支持多选,保存后将以用户名写入中心联系人字段</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="onSave">保存绑定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { updateSite } from "../../api/sites";
|
||||
import type { Site, UserInfo } from "../../types/api";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
studyId: string;
|
||||
site: Site | null;
|
||||
craUsers: UserInfo[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "saved"): void;
|
||||
}>();
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (val: boolean) => emit("update:visible", val),
|
||||
});
|
||||
|
||||
const selectedCras = ref<string[]>([]);
|
||||
const saving = ref(false);
|
||||
|
||||
const loadSelected = () => {
|
||||
if (!props.site) {
|
||||
selectedCras.value = [];
|
||||
return;
|
||||
}
|
||||
const raw = props.site.contact || "";
|
||||
const tokens = raw
|
||||
.split(",")
|
||||
.map((i) => i.trim())
|
||||
.filter(Boolean);
|
||||
selectedCras.value = tokens
|
||||
.map((token) => {
|
||||
const byId = props.craUsers.find((u) => u.id === token);
|
||||
if (byId) return byId.id;
|
||||
const byName = props.craUsers.find((u) => u.username === token);
|
||||
return byName?.id;
|
||||
})
|
||||
.filter((v): v is string => !!v);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadSelected();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onSave = async () => {
|
||||
if (!props.site) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const names = selectedCras.value
|
||||
.map((id) => props.craUsers.find((u) => u.id === id)?.username || id)
|
||||
.filter(Boolean);
|
||||
await updateSite(props.studyId, props.site.id, { contact: names.join(",") });
|
||||
ElMessage.success("绑定已保存");
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tip {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.hint {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<el-dialog :title="site ? '编辑中心' : '新建中心'" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="中心名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入中心名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="城市" prop="city">
|
||||
<el-input v-model="form.city" placeholder="所在城市(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="PI" prop="pi_name">
|
||||
<el-input v-model="form.pi_name" placeholder="PI(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="CRA 联系人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="多个联系人用逗号分隔" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="site" label="状态" prop="is_active">
|
||||
<el-switch v-model="form.is_active" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { createSite, updateSite } from "../../api/sites";
|
||||
import type { Site } from "../../types/api";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
studyId: string;
|
||||
site?: Site | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "saved"): void;
|
||||
}>();
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (val: boolean) => emit("update:visible", val),
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
name: "",
|
||||
city: "",
|
||||
pi_name: "",
|
||||
contact: "",
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: "请输入中心名称", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
form.name = "";
|
||||
form.city = "";
|
||||
form.pi_name = "";
|
||||
form.contact = "";
|
||||
form.is_active = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.site) {
|
||||
form.name = props.site.name;
|
||||
form.city = props.site.city || "";
|
||||
form.pi_name = props.site.pi_name || "";
|
||||
form.contact = props.site.contact || "";
|
||||
form.is_active = props.site.is_active;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate();
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (props.site) {
|
||||
await updateSite(props.studyId, props.site.id, {
|
||||
name: form.name,
|
||||
city: form.city,
|
||||
pi_name: form.pi_name,
|
||||
contact: form.contact,
|
||||
is_active: form.is_active,
|
||||
});
|
||||
ElMessage.success("中心已更新");
|
||||
} else {
|
||||
await createSite(props.studyId, {
|
||||
name: form.name,
|
||||
city: form.city,
|
||||
pi_name: form.pi_name,
|
||||
contact: form.contact,
|
||||
});
|
||||
ElMessage.success("中心已创建");
|
||||
}
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3>中心管理</h3>
|
||||
<div class="sub" v-if="project">项目:{{ project.code }} - {{ project.name }}</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">新建中心</el-button>
|
||||
</div>
|
||||
<el-table :data="sites" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" label="中心名称" min-width="180" />
|
||||
<el-table-column prop="city" label="城市" width="140" />
|
||||
<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="120">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.is_active">启用</el-tag>
|
||||
<el-tag type="danger" v-else>停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="scope">
|
||||
<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
|
||||
link
|
||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||
size="small"
|
||||
@click="toggleSite(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<SiteForm
|
||||
v-if="projectId"
|
||||
v-model:visible="formVisible"
|
||||
:study-id="projectId"
|
||||
:site="editingSite"
|
||||
@saved="loadSites"
|
||||
/>
|
||||
<SiteCraBinding
|
||||
v-if="projectId"
|
||||
v-model:visible="bindVisible"
|
||||
:study-id="projectId"
|
||||
:site="bindingSite"
|
||||
:cra-users="craUsers"
|
||||
@saved="loadSites"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { fetchStudyDetail } from "../../api/studies";
|
||||
import { fetchSites, updateSite } from "../../api/sites";
|
||||
import { fetchUsers } from "../../api/users";
|
||||
import SiteForm from "./SiteForm.vue";
|
||||
import SiteCraBinding from "./SiteCraBinding.vue";
|
||||
import type { Site, Study, UserInfo } from "../../types/api";
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = computed(() => route.params.projectId as string);
|
||||
|
||||
const project = ref<Study | null>(null);
|
||||
const sites = ref<Site[]>([]);
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const formVisible = ref(false);
|
||||
const editingSite = ref<Site | null>(null);
|
||||
const bindVisible = ref(false);
|
||||
const bindingSite = ref<Site | null>(null);
|
||||
|
||||
const loadProject = async () => {
|
||||
if (!projectId.value) return;
|
||||
try {
|
||||
const { data } = await fetchStudyDetail(projectId.value);
|
||||
project.value = data;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const loadSites = async () => {
|
||||
if (!projectId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await fetchSites(projectId.value);
|
||||
sites.value = Array.isArray(data) ? data : (data as any).items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "中心列表加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
const { data } = await fetchUsers({ limit: 500 });
|
||||
users.value = (data as any).items || [];
|
||||
} catch {
|
||||
users.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const craUsers = computed(() => users.value.filter((u) => u.role === "CRA" && u.is_active));
|
||||
|
||||
const openCreate = () => {
|
||||
editingSite.value = null;
|
||||
formVisible.value = true;
|
||||
};
|
||||
|
||||
const openEdit = (row: Site) => {
|
||||
editingSite.value = row;
|
||||
formVisible.value = true;
|
||||
};
|
||||
|
||||
const openBind = (row: Site) => {
|
||||
bindingSite.value = row;
|
||||
bindVisible.value = true;
|
||||
};
|
||||
|
||||
const toggleSite = async (row: Site) => {
|
||||
if (!projectId.value) return;
|
||||
if (row.is_active) {
|
||||
const ok = await ElMessageBox.confirm("该操作将影响中心数据,请确认是否继续", "停用中心", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确认",
|
||||
}).catch(() => null);
|
||||
if (!ok) return;
|
||||
}
|
||||
try {
|
||||
await updateSite(projectId.value, row.id, { is_active: !row.is_active });
|
||||
ElMessage.success(row.is_active ? "已停用" : "已启用");
|
||||
loadSites();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadProject(), loadUsers()]);
|
||||
loadSites();
|
||||
});
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<el-dialog :title="user ? '编辑用户' : '新建用户'" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" :disabled="!!user" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" prop="role">
|
||||
<el-select v-model="form.role" placeholder="请选择角色">
|
||||
<el-option label="ADMIN" value="ADMIN" />
|
||||
<el-option label="PM" value="PM" />
|
||||
<el-option label="CRA" value="CRA" />
|
||||
<el-option label="PV" value="PV" />
|
||||
<el-option label="IMP" value="IMP" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="user ? '新密码(可选)' : '初始密码'" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password :placeholder="user ? '留空则不修改密码' : '请输入初始密码'" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="user" label="账号状态" prop="is_active">
|
||||
<el-switch v-model="form.is_active" active-text="启用" inactive-text="禁用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { createUser, updateUser } from "../../api/users";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
user?: UserInfo | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "saved"): void;
|
||||
}>();
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (val: boolean) => emit("update:visible", val),
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
username: "",
|
||||
role: "PM",
|
||||
password: "",
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
role: [{ required: true, message: "请选择角色", trigger: "change" }],
|
||||
password: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!props.user && !value) {
|
||||
callback(new Error("请输入初始密码"));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
form.username = "";
|
||||
form.role = "PM";
|
||||
form.password = "";
|
||||
form.is_active = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.user) {
|
||||
form.username = props.user.username;
|
||||
form.role = props.user.role;
|
||||
form.is_active = props.user.is_active;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate();
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (props.user) {
|
||||
await updateUser(props.user.id, {
|
||||
role: form.role,
|
||||
is_active: form.is_active,
|
||||
password: form.password || undefined,
|
||||
});
|
||||
ElMessage.success("用户已更新");
|
||||
} else {
|
||||
await createUser({
|
||||
username: form.username,
|
||||
role: form.role,
|
||||
password: form.password,
|
||||
});
|
||||
ElMessage.success("用户已创建");
|
||||
}
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<el-dialog title="重置密码" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
|
||||
<el-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
title="该操作将影响用户账号,请确认是否继续"
|
||||
description="系统将生成临时密码,需再次输入用户名才能提交。"
|
||||
class="mb-12"
|
||||
/>
|
||||
<div class="info">重置对象:<strong>{{ user?.username }}</strong></div>
|
||||
<el-form ref="formRef" :model="form" label-width="140px" :rules="rules">
|
||||
<el-form-item label="确认用户名" prop="confirmUsername">
|
||||
<el-input v-model="form.confirmUsername" placeholder="请输入用户名以确认" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码方式" prop="mode">
|
||||
<el-radio-group v-model="form.mode">
|
||||
<el-radio label="auto">系统生成临时密码</el-radio>
|
||||
<el-radio label="manual">手动输入新密码</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="form.mode === 'auto' ? '临时密码' : '新密码'" prop="passwordValue">
|
||||
<el-input
|
||||
v-if="form.mode === 'auto'"
|
||||
v-model="form.tempPassword"
|
||||
type="password"
|
||||
show-password
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="regenerate">重新生成</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="form.manualPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
<div class="hint">不会明文展示,请通过安全渠道告知用户</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="danger" :disabled="!canSubmit" :loading="submitting" @click="onSubmit">确认重置</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||
import { updateUser } from "../../api/users";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
user: UserInfo | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "reset"): void;
|
||||
}>();
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: (val: boolean) => emit("update:visible", val),
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
confirmUsername: "",
|
||||
mode: "auto",
|
||||
tempPassword: "",
|
||||
manualPassword: "",
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
confirmUsername: [{ required: true, message: "请再次输入用户名", trigger: "blur" }],
|
||||
mode: [{ required: true, message: "请选择密码方式", trigger: "change" }],
|
||||
passwordValue: [
|
||||
{
|
||||
validator: (_rule, _value, callback) => {
|
||||
if (form.mode === "auto" && !form.tempPassword) {
|
||||
callback(new Error("临时密码生成异常"));
|
||||
return;
|
||||
}
|
||||
if (form.mode === "manual" && !form.manualPassword) {
|
||||
callback(new Error("请输入新密码"));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const generatePassword = () => {
|
||||
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%^&*";
|
||||
let pwd = "";
|
||||
for (let i = 0; i < 12; i++) {
|
||||
pwd += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
form.tempPassword = pwd;
|
||||
};
|
||||
|
||||
const regenerate = () => generatePassword();
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
form.confirmUsername = "";
|
||||
form.mode = "auto";
|
||||
generatePassword();
|
||||
form.manualPassword = "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const canSubmit = computed(() => !!props.user && form.confirmUsername === props.user.username);
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!props.user) return;
|
||||
await formRef.value?.validate();
|
||||
if (form.confirmUsername !== props.user.username) {
|
||||
ElMessage.error("确认用户名不匹配,无法提交");
|
||||
return;
|
||||
}
|
||||
const ok = await ElMessageBox.confirm(
|
||||
"该操作将影响用户账号,请确认是否继续",
|
||||
"确认重置密码",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确认重置",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
const password = form.mode === "manual" ? form.manualPassword : form.tempPassword;
|
||||
await updateUser(props.user.id, { password });
|
||||
ElMessage.success("密码已重置");
|
||||
emit("reset");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "重置失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb-12 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.info {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<h3>用户账号管理</h3>
|
||||
<el-button type="primary" @click="openCreate">新建用户</el-button>
|
||||
</div>
|
||||
<el-table :data="users" stripe v-loading="loading" style="width: 100%">
|
||||
<el-table-column prop="username" label="用户名" min-width="140" />
|
||||
<el-table-column prop="role" label="角色" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.role === 'ADMIN'">ADMIN</el-tag>
|
||||
<el-tag v-else>{{ scope.row.role }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag type="success" v-if="scope.row.is_active">启用</el-tag>
|
||||
<el-tag type="danger" v-else>已禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="200" />
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.is_active ? 'danger' : 'primary'"
|
||||
size="small"
|
||||
@click="toggleStatus(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? "禁用" : "启用" }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="openReset(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" @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 } from "../../api/users";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
import UserForm from "./UserForm.vue";
|
||||
import UserResetPassword from "./UserResetPassword.vue";
|
||||
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const loading = ref(false);
|
||||
const formVisible = ref(false);
|
||||
const resetVisible = ref(false);
|
||||
const editingUser = ref<UserInfo | null>(null);
|
||||
const resetUser = ref<UserInfo | null>(null);
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await fetchUsers({ skip: (page.value - 1) * pageSize.value, limit: pageSize.value });
|
||||
const items = (data as any).items || [];
|
||||
users.value = items;
|
||||
total.value = (data as any).total || items.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) => {
|
||||
const disable = row.is_active;
|
||||
const ok = await ElMessageBox.confirm(
|
||||
disable ? "该操作将影响用户账号,请确认是否继续" : "确认启用该用户账号?",
|
||||
disable ? "禁用账号" : "启用账号",
|
||||
{
|
||||
type: disable ? "warning" : "info",
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await updateUser(row.id, { is_active: !row.is_active });
|
||||
ElMessage.success(disable ? "已禁用" : "已启用");
|
||||
loadUsers();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
const openReset = (row: UserInfo) => {
|
||||
resetUser.value = row;
|
||||
resetVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding: 16px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pagination {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user