全局中文化

This commit is contained in:
Cheng Zhou
2026-01-12 11:14:04 +08:00
parent 9021c7fe2b
commit 1338fa7e2b
103 changed files with 2443 additions and 1533 deletions
+26 -23
View File
@@ -3,30 +3,32 @@
<el-card>
<div class="header">
<div>
<h3>注册审核</h3>
<p>仅显示待审核账号审核通过后即可登录</p>
<h3>{{ TEXT.modules.adminUserApproval.title }}</h3>
<p>{{ TEXT.modules.adminUserApproval.subtitle }}</p>
</div>
<el-select v-model="status" size="small" style="width: 180px" @change="loadUsers">
<el-option label="待审核" value="PENDING" />
<el-option label="已通过" value="ACTIVE" />
<el-option label="已拒绝" value="REJECTED" />
<el-option label="已禁用" value="DISABLED" />
<el-option :label="TEXT.enums.userStatus.PENDING" value="PENDING" />
<el-option :label="TEXT.enums.userStatus.ACTIVE" value="ACTIVE" />
<el-option :label="TEXT.enums.userStatus.REJECTED" value="REJECTED" />
<el-option :label="TEXT.enums.userStatus.DISABLED" value="DISABLED" />
</el-select>
</div>
<el-table :data="users" 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="role" label="角色" width="120" />
<el-table-column prop="department" label="部门" min-width="160" />
<el-table-column prop="created_at" label="注册时间" width="200">
<el-table-column prop="email" :label="TEXT.common.fields.email" min-width="200" />
<el-table-column prop="full_name" :label="TEXT.common.fields.name" min-width="140" />
<el-table-column prop="role" :label="TEXT.common.labels.role" width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.userRole, scope.row.role) }}</template>
</el-table-column>
<el-table-column prop="department" :label="TEXT.common.fields.department" min-width="160" />
<el-table-column prop="created_at" :label="TEXT.modules.adminUserApproval.createdAt" width="200">
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<template #default="scope">
<el-tag :type="statusType(scope.row.status)">{{ scope.row.status }}</el-tag>
<el-tag :type="statusType(scope.row.status)">{{ displayEnum(TEXT.enums.userStatus, scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="220" fixed="right">
<template #default="scope">
<el-button
v-if="scope.row.status === 'PENDING'"
@@ -35,7 +37,7 @@
size="small"
@click="onApprove(scope.row.id)"
>
审核通过
{{ TEXT.modules.adminUserApproval.approve }}
</el-button>
<el-button
v-if="scope.row.status === 'PENDING'"
@@ -44,9 +46,9 @@
size="small"
@click="onReject(scope.row.id)"
>
拒绝
{{ TEXT.modules.adminUserApproval.reject }}
</el-button>
<span v-else></span>
<span v-else>{{ TEXT.common.fallback }}</span>
</template>
</el-table-column>
</el-table>
@@ -59,7 +61,8 @@ import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { approveUser, listPendingUsers, rejectUser } from "../../api/admin";
import type { UserInfo, UserStatus } from "../../types/api";
import { displayDateTime } from "../../utils/display";
import { displayDateTime, displayEnum } from "../../utils/display";
import { TEXT } from "../../locales";
const users = ref<UserInfo[]>([]);
const loading = ref(false);
@@ -84,7 +87,7 @@ const loadUsers = async () => {
const { data } = await listPendingUsers(status.value);
users.value = data.items || [];
} catch (error: any) {
ElMessage.error(error?.response?.data?.message || "加载待审核用户失败");
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.loadFailed);
} finally {
loading.value = false;
}
@@ -93,20 +96,20 @@ const loadUsers = async () => {
const onApprove = async (id: string) => {
try {
await approveUser(id);
ElMessage.success("已通过审核");
ElMessage.success(TEXT.modules.adminUserApproval.approveSuccess);
loadUsers();
} catch (error: any) {
ElMessage.error(error?.response?.data?.message || "审核失败");
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.approveFailed);
}
};
const onReject = async (id: string) => {
try {
await rejectUser(id);
ElMessage.success("已拒绝");
ElMessage.success(TEXT.modules.adminUserApproval.rejectSuccess);
loadUsers();
} catch (error: any) {
ElMessage.error(error?.response?.data?.message || "拒绝失败");
ElMessage.error(error?.response?.data?.message || TEXT.modules.adminUserApproval.rejectFailed);
}
};
+29 -28
View File
@@ -3,22 +3,22 @@
<el-card>
<div class="ctms-table-toolbar">
<div class="ctms-filter-group">
<el-select v-model="filters.eventType" clearable placeholder="操作类型" @change="loadLogs" class="ctms-filter-item">
<el-select v-model="filters.eventType" clearable :placeholder="TEXT.modules.adminAuditLogs.filterEvent" @change="loadLogs" class="ctms-filter-item">
<el-option v-for="opt in eventTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
<el-select v-model="filters.operatorId" clearable placeholder="操作人" filterable @change="loadLogs" class="ctms-filter-item">
<el-select v-model="filters.operatorId" clearable :placeholder="TEXT.modules.adminAuditLogs.filterOperator" filterable @change="loadLogs" class="ctms-filter-item">
<el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
</el-select>
<el-select v-model="filters.result" clearable placeholder="结果" @change="loadLogs" style="width: 100px">
<el-option label="成功" value="SUCCESS" />
<el-option label="失败" value="FAIL" />
<el-select v-model="filters.result" clearable :placeholder="TEXT.modules.adminAuditLogs.filterResult" @change="loadLogs" style="width: 100px">
<el-option :label="TEXT.audit.resultSuccess" value="SUCCESS" />
<el-option :label="TEXT.audit.resultFail" value="FAIL" />
</el-select>
<el-date-picker
v-model="filters.range"
type="daterange"
unlink-panels
start-placeholder="开始"
end-placeholder="结束"
:start-placeholder="TEXT.modules.adminAuditLogs.rangeStart"
:end-placeholder="TEXT.modules.adminAuditLogs.rangeEnd"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 240px"
@@ -34,7 +34,7 @@
:loading="exportLoading"
@click="confirmExport('system')"
>
导出系统审计
{{ TEXT.modules.adminAuditLogs.exportSystem }}
</el-button>
<el-button
v-if="canProjectExport"
@@ -44,36 +44,36 @@
:loading="exportLoading"
@click="confirmExport('project')"
>
导出项目审计
{{ TEXT.modules.adminAuditLogs.exportProject }}
</el-button>
</div>
</div>
<el-table :data="logs" v-loading="loading" style="width: 100%">
<el-table-column prop="timestamp" label="时间" width="180">
<el-table-column prop="timestamp" :label="TEXT.modules.adminAuditLogs.columns.time" width="180">
<template #default="scope">{{ displayDateTime(scope.row.timestamp) }}</template>
</el-table-column>
<el-table-column prop="actorName" label="操作人" width="140" />
<el-table-column prop="actorRoleLabel" label="角色" width="120" />
<el-table-column prop="eventLabel" label="操作类型" min-width="160" />
<el-table-column prop="actionText" label="操作内容" min-width="180" />
<el-table-column label="操作对象" min-width="180">
<el-table-column prop="actorName" :label="TEXT.modules.adminAuditLogs.columns.actor" width="140" />
<el-table-column prop="actorRoleLabel" :label="TEXT.common.labels.role" width="120" />
<el-table-column prop="eventLabel" :label="TEXT.modules.adminAuditLogs.columns.event" min-width="160" />
<el-table-column prop="actionText" :label="TEXT.modules.adminAuditLogs.columns.action" min-width="180" />
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.target" min-width="180">
<template #default="scope">
<span v-if="scope.row.targetTypeLabel">
{{ scope.row.targetTypeLabel }}{{ scope.row.targetName || "" }}
{{ scope.row.targetTypeLabel }}{{ scope.row.targetName || TEXT.common.fallback }}
</span>
<span v-else></span>
<span v-else>{{ TEXT.common.fallback }}</span>
</template>
</el-table-column>
<el-table-column label="变更详情" min-width="220">
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.diff" min-width="220">
<template #default="scope">
<div v-if="scope.row.diffText?.length">
<div v-for="(line, idx) in scope.row.diffText" :key="idx">{{ line }}</div>
</div>
<span v-else>-</span>
<span v-else>{{ TEXT.audit.emptyValue }}</span>
</template>
</el-table-column>
<el-table-column label="结果" width="120">
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="120">
<template #default="scope">
<el-tag :type="scope.row.result === 'SUCCESS' ? 'success' : 'danger'">
{{ scope.row.resultLabel }}
@@ -107,6 +107,7 @@ import { roleDict, getDictLabel } from "../../dictionaries";
import { exportAuditCsv } from "../../audit/export/auditExportService";
import { logAudit } from "../../audit";
import { displayDateTime } from "../../utils/display";
import { TEXT } from "../../locales";
const buildLogKey = (log: any) => {
if (log.id) return `id:${log.id}`;
@@ -197,7 +198,7 @@ const loadLogs = async () => {
total.value = (data as any).total || merged.length;
enrichLogs();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "审计日志加载失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.loadFailed);
} finally {
loading.value = false;
}
@@ -278,7 +279,7 @@ const fetchAllForExport = async () => {
});
return filtered.map((log) => normalizeAuditEvent(log, userMap));
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "导出数据加载失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminAuditLogs.exportLoadFailed);
return [];
} finally {
exportLoading.value = false;
@@ -287,20 +288,20 @@ const fetchAllForExport = async () => {
const confirmExport = async (scope: "system" | "project") => {
const ok = await ElMessageBox.confirm(
"该文件包含敏感审计信息,仅限授权人员使用。确认导出吗?",
"审计导出确认",
{ type: "warning", confirmButtonText: "确认", cancelButtonText: "取消" }
TEXT.modules.adminAuditLogs.exportConfirm,
TEXT.modules.adminAuditLogs.exportConfirmTitle,
{ type: "warning", confirmButtonText: TEXT.common.actions.confirm, cancelButtonText: TEXT.common.actions.cancel }
).catch(() => null);
if (!ok) return;
const events = await fetchAllForExport();
if (!events.length) {
ElMessage.warning("无可导出的审计记录");
ElMessage.warning(TEXT.modules.adminAuditLogs.exportEmpty);
return;
}
const fileName =
scope === "system"
? `系统审计_${new Date().toISOString().slice(0, 10)}`
: `项目审计_${study.currentStudy?.code || ""}_${new Date().toISOString().slice(0, 10)}`;
? `${TEXT.modules.adminAuditLogs.exportSystemName}_${new Date().toISOString().slice(0, 10)}`
: `${TEXT.modules.adminAuditLogs.exportProjectName}_${study.currentStudy?.code || ""}_${new Date().toISOString().slice(0, 10)}`;
exportAuditCsv(events, { fileName });
logAudit(scope === "system" ? "AUDIT_EXPORT_SYSTEM" : "AUDIT_EXPORT_PROJECT", {
targetId: study.currentStudy?.id,
+14 -17
View File
@@ -2,27 +2,27 @@
<div class="ctms-page">
<div class="ctms-page-header">
<div>
<h1 class="ctms-page-title">项目详情</h1>
<h1 class="ctms-page-title">{{ TEXT.modules.adminProjects.detailTitle }}</h1>
</div>
<div class="ctms-page-actions">
<el-button type="primary" @click="enterProject" :disabled="!project">进入项目</el-button>
<el-button @click="goMembers" :disabled="!project">项目成员配置</el-button>
<el-button @click="goSites" :disabled="!project">中心管理</el-button>
<el-button type="primary" @click="enterProject" :disabled="!project">{{ TEXT.modules.adminProjects.enter }}</el-button>
<el-button @click="goMembers" :disabled="!project">{{ TEXT.modules.adminProjects.members }}</el-button>
<el-button @click="goSites" :disabled="!project">{{ TEXT.modules.adminProjects.sites }}</el-button>
</div>
</div>
<el-card class="ctms-section-card">
<template #header>
<div>
<div class="ctms-section-title">项目基本信息</div>
<div class="ctms-section-title">{{ TEXT.modules.adminProjects.basicInfo }}</div>
</div>
</template>
<el-descriptions v-if="project" :column="2" border class="detail-descriptions">
<el-descriptions-item label="项目编号">{{ project.code }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ project.name }}</el-descriptions-item>
<el-descriptions-item label="申办方">{{ project.sponsor || "—" }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ statusLabel(project.status) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ displayDateTime(project.created_at) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.projectCode">{{ project.code }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.projectName">{{ project.name }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.sponsor">{{ project.sponsor || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">{{ statusLabel(project.status) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.adminProjects.createdAt">{{ displayDateTime(project.created_at) }}</el-descriptions-item>
</el-descriptions>
<StateLoading v-else :rows="4" />
</el-card>
@@ -30,7 +30,7 @@
<el-card class="ctms-section-card">
<template #header>
<div>
<div class="ctms-section-title">项目通用文件</div>
<div class="ctms-section-title">{{ TEXT.modules.adminProjects.filesTitle }}</div>
</div>
</template>
<AttachmentList
@@ -55,6 +55,7 @@ import AttachmentList from "../../components/attachments/AttachmentList.vue";
import StateLoading from "../../components/StateLoading.vue";
import { useStudyStore } from "../../store/study";
import { displayDateTime } from "../../utils/display";
import { TEXT } from "../../locales";
const route = useRoute();
const router = useRouter();
@@ -67,16 +68,12 @@ const loadProject = async () => {
const { data } = await fetchStudyDetail(route.params.projectId as string);
project.value = data as Study;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "项目加载失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjects.loadFailed);
}
};
const statusLabel = (status: string) =>
({
DRAFT: "草稿",
ACTIVE: "进行中",
CLOSED: "已关闭",
}[status] || status);
TEXT.enums.projectStatus[status as keyof typeof TEXT.enums.projectStatus] || status;
const enterProject = () => {
if (!project.value) return;
+25 -24
View File
@@ -1,32 +1,32 @@
<template>
<el-dialog :title="project ? '编辑项目' : '新建项目'" width="560px" v-model="visibleProxy" :close-on-click-modal="false">
<el-dialog :title="project ? TEXT.modules.adminProjects.editTitle : TEXT.modules.adminProjects.newTitle" 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 :label="TEXT.common.fields.projectCode" prop="code">
<el-input v-model="form.code" :disabled="!!project" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectCode" />
</el-form-item>
<el-form-item label="项目名称" prop="name">
<el-input v-model="form.name" placeholder="请输入项目名称" />
<el-form-item :label="TEXT.common.fields.projectName" prop="name">
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectName" />
</el-form-item>
<el-form-item label="申办方" prop="sponsor">
<el-input v-model="form.sponsor" placeholder="申办方(可选)" />
<el-form-item :label="TEXT.common.fields.sponsor" prop="sponsor">
<el-input v-model="form.sponsor" :placeholder="TEXT.modules.adminProjects.sponsorPlaceholder" />
</el-form-item>
<el-form-item label="方案号" prop="protocol_no">
<el-input v-model="form.protocol_no" placeholder="方案号(可选)" />
<el-form-item :label="TEXT.common.fields.protocolNo" prop="protocol_no">
<el-input v-model="form.protocol_no" :placeholder="TEXT.modules.adminProjects.protocolPlaceholder" />
</el-form-item>
<el-form-item label="分期" prop="phase">
<el-input v-model="form.phase" placeholder="研究分期(可选)" />
<el-form-item :label="TEXT.common.fields.phase" prop="phase">
<el-input v-model="form.phase" :placeholder="TEXT.modules.adminProjects.phasePlaceholder" />
</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-form-item :label="TEXT.common.fields.status" prop="status">
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
<el-option :label="TEXT.enums.projectStatus.DRAFT" value="DRAFT" />
<el-option :label="TEXT.enums.projectStatus.ACTIVE" value="ACTIVE" />
<el-option :label="TEXT.enums.projectStatus.CLOSED" 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>
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
</template>
</el-dialog>
</template>
@@ -36,6 +36,7 @@ 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";
import { TEXT, requiredMessage } from "../../locales";
const props = defineProps<{
visible: boolean;
@@ -64,9 +65,9 @@ const form = reactive({
});
const rules = reactive<FormRules>({
code: [{ required: true, message: "请输入项目编号", trigger: "blur" }],
name: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
status: [{ required: true, message: "请选择项目状态", trigger: "change" }],
code: [{ required: true, message: requiredMessage(TEXT.common.fields.projectCode), trigger: "blur" }],
name: [{ required: true, message: requiredMessage(TEXT.common.fields.projectName), trigger: "blur" }],
status: [{ required: true, message: requiredMessage(TEXT.common.fields.status), trigger: "change" }],
});
const resetForm = () => {
@@ -108,7 +109,7 @@ const onSubmit = async () => {
phase: form.phase,
status: form.status,
});
ElMessage.success("项目已更新");
ElMessage.success(TEXT.modules.adminProjects.updateSuccess);
} else {
await createStudy({
code: form.code,
@@ -118,12 +119,12 @@ const onSubmit = async () => {
phase: form.phase,
status: form.status,
});
ElMessage.success("项目已创建");
ElMessage.success(TEXT.modules.adminProjects.createSuccess);
}
emit("saved");
visibleProxy.value = false;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "保存失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
} finally {
submitting.value = false;
}
+57 -56
View File
@@ -3,37 +3,37 @@
<el-card>
<div class="header">
<div>
<h3>项目成员配置角色仅在当前项目内生效</h3>
<div class="sub" v-if="project">项目{{ project.code }} - {{ project.name }}</div>
<div class="sub">同一账号在不同项目可拥有不同角色角色授权仅在本页进行</div>
<h3>{{ TEXT.modules.adminProjectMembers.title }}</h3>
<div class="sub" v-if="project">{{ TEXT.modules.adminProjectMembers.projectPrefix }}{{ project.code }} - {{ project.name }}</div>
<div class="sub">{{ TEXT.modules.adminProjectMembers.subtitle }}</div>
</div>
<el-button type="primary" @click="openAdd">新增成员</el-button>
<el-button type="primary" @click="openAdd">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjectMembers.memberLabel }}</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="140">
<el-table-column prop="username" :label="TEXT.modules.adminProjectMembers.username" min-width="160" />
<el-table-column prop="role_in_study" :label="TEXT.modules.adminProjectMembers.projectRole" width="140">
<template #default="scope">
<el-tag>{{ scope.row.role_in_study }}</el-tag>
<el-tag>{{ displayEnum(TEXT.enums.userRole, scope.row.role_in_study) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="160">
<el-table-column :label="TEXT.common.fields.status" width="160">
<template #default="scope">
<el-tooltip
v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
content="请前往【账号治理】启用该账号"
:content="TEXT.modules.adminProjectMembers.disabledGlobalHint"
placement="top"
>
<el-tag type="danger">全局已禁用</el-tag>
<el-tag type="danger">{{ TEXT.modules.adminProjectMembers.disabledGlobal }}</el-tag>
</el-tooltip>
<el-tag v-else :type="scope.row.is_active ? 'success' : 'danger'">
{{ scope.row.is_active ? "启用" : "已禁用" }}
{{ scope.row.is_active ? TEXT.common.actions.enable : TEXT.modules.adminProjectMembers.disabled }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="added_at" label="加入时间" width="200">
<el-table-column prop="added_at" :label="TEXT.modules.adminProjectMembers.addedAt" width="200">
<template #default="scope">{{ displayDateTime(scope.row.added_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="360" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="360" fixed="right">
<template #default="scope">
<el-select
v-model="scope.row.role_in_study"
@@ -42,14 +42,14 @@
@change="(val) => updateRole(scope.row.id, val)"
:disabled="!scope.row.is_active || scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
>
<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-option label="ADMIN" value="ADMIN" />
<el-option :label="TEXT.enums.userRole.PM" value="PM" />
<el-option :label="TEXT.enums.userRole.CRA" value="CRA" />
<el-option :label="TEXT.enums.userRole.PV" value="PV" />
<el-option :label="TEXT.enums.userRole.IMP" value="IMP" />
<el-option :label="TEXT.enums.userRole.ADMIN" value="ADMIN" />
</el-select>
<span v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'" class="hint">该账号已在系统级被禁用无法在任何项目中使用</span>
<el-button link type="danger" size="small" @click="onDelete(scope.row)">删除</el-button>
<span v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'" class="hint">{{ TEXT.modules.adminProjectMembers.disabledGlobalDesc }}</span>
<el-button link type="danger" size="small" @click="onDelete(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
<el-button
link
:type="scope.row.is_active ? 'danger' : 'primary'"
@@ -57,17 +57,17 @@
:disabled="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
@click="toggleActive(scope.row)"
>
{{ scope.row.is_active ? "停用" : "启用" }}
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog title="新增成员" width="520px" v-model="addVisible" :close-on-click-modal="false">
<el-dialog :title="TEXT.modules.adminProjectMembers.newTitle" 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-form-item :label="TEXT.modules.adminProjectMembers.user" prop="user_id">
<el-select v-model="newMember.user_id" filterable :placeholder="TEXT.modules.adminProjectMembers.userPlaceholder">
<el-option
v-for="user in availableUsers"
:key="user.id"
@@ -77,19 +77,19 @@
/>
</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-option label="PV" value="PV" />
<el-option label="IMP" value="IMP" />
<el-option label="ADMIN" value="ADMIN" />
<el-form-item :label="TEXT.modules.adminProjectMembers.projectRole" prop="role_in_study">
<el-select v-model="newMember.role_in_study" :placeholder="TEXT.modules.adminProjectMembers.rolePlaceholder">
<el-option :label="TEXT.enums.userRole.PM" value="PM" />
<el-option :label="TEXT.enums.userRole.CRA" value="CRA" />
<el-option :label="TEXT.enums.userRole.PV" value="PV" />
<el-option :label="TEXT.enums.userRole.IMP" value="IMP" />
<el-option :label="TEXT.enums.userRole.ADMIN" value="ADMIN" />
</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>
<el-button @click="addVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="adding" @click="submitAdd">{{ TEXT.common.actions.save }}</el-button>
</template>
</el-dialog>
</div>
@@ -106,7 +106,8 @@ import type { Study, StudyMember, UserInfo } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { displayDateTime } from "../../utils/display";
import { displayDateTime, displayEnum } from "../../utils/display";
import { TEXT, requiredMessage } from "../../locales";
const route = useRoute();
const projectId = computed(() => route.params.projectId as string);
@@ -125,8 +126,8 @@ const newMember = reactive({
});
const addRules = reactive<FormRules>({
user_id: [{ required: true, message: "请选择用户", trigger: "change" }],
role_in_study: [{ required: true, message: "请选择角色", trigger: "change" }],
user_id: [{ required: true, message: requiredMessage(TEXT.modules.adminProjectMembers.user), trigger: "change" }],
role_in_study: [{ required: true, message: requiredMessage(TEXT.modules.adminProjectMembers.projectRole), trigger: "change" }],
});
const loadProject = async () => {
@@ -146,7 +147,7 @@ const loadMembers = async () => {
const { data } = await listMembers(projectId.value, { limit: 500 });
members.value = Array.isArray(data) ? data : data.items || [];
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "成员列表加载失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjectMembers.loadFailed);
} finally {
loading.value = false;
}
@@ -187,7 +188,7 @@ const submitAdd = async () => {
target: { projectId: projectId.value },
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || "无权限执行该操作");
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: projectId.value,
targetName: project.value?.name,
@@ -200,7 +201,7 @@ const submitAdd = async () => {
adding.value = true;
try {
await addMember(projectId.value, newMember);
ElMessage.success("成员已添加");
ElMessage.success(TEXT.modules.adminProjectMembers.addSuccess);
addVisible.value = false;
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
@@ -210,7 +211,7 @@ const submitAdd = async () => {
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "添加失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjectMembers.addFailed);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
@@ -231,7 +232,7 @@ const updateRole = async (memberId: string, role: string) => {
target: { projectId: projectId.value, memberId },
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || "无权限执行该操作");
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: projectId.value,
targetName: project.value?.name,
@@ -242,7 +243,7 @@ const updateRole = async (memberId: string, role: string) => {
}
try {
await updateMember(projectId.value, memberId, { role_in_study: role });
ElMessage.success("角色已更新");
ElMessage.success(TEXT.modules.adminProjectMembers.roleUpdated);
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
@@ -251,7 +252,7 @@ const updateRole = async (memberId: string, role: string) => {
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "更新失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.updateFailed);
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
@@ -271,7 +272,7 @@ const toggleActive = async (row: StudyMember) => {
target: { projectId: projectId.value, memberId: row.id },
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || "无权限执行该操作");
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: projectId.value,
targetName: project.value?.name,
@@ -281,14 +282,14 @@ const toggleActive = async (row: StudyMember) => {
return;
}
if (row.is_active) {
const ok = await ElMessageBox.confirm("该操作将影响项目成员,请确认是否继续", "停用成员", {
const ok = await ElMessageBox.confirm(TEXT.modules.adminProjectMembers.disableConfirm, TEXT.modules.adminProjectMembers.disableTitle, {
type: "warning",
confirmButtonText: "确认",
confirmButtonText: TEXT.common.actions.confirm,
}).catch(() => null);
if (!ok) return;
try {
await updateMember(projectId.value, row.id, { is_active: false });
ElMessage.success("成员已停用");
ElMessage.success(TEXT.modules.adminProjectMembers.disableSuccess);
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
@@ -298,7 +299,7 @@ const toggleActive = async (row: StudyMember) => {
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "操作失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
@@ -311,7 +312,7 @@ const toggleActive = async (row: StudyMember) => {
} else {
try {
await updateMember(projectId.value, row.id, { is_active: true });
ElMessage.success("成员已启用");
ElMessage.success(TEXT.modules.adminProjectMembers.enableSuccess);
loadMembers();
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
@@ -321,7 +322,7 @@ const toggleActive = async (row: StudyMember) => {
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "操作失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
@@ -342,7 +343,7 @@ const onDelete = async (row: StudyMember) => {
target: { projectId: projectId.value, memberId: row.id },
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || "无权限执行该操作");
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: projectId.value,
targetName: project.value?.name,
@@ -351,16 +352,16 @@ const onDelete = async (row: StudyMember) => {
});
return;
}
const ok = await ElMessageBox.confirm("确认将该账号从本项目移除?账号本身不会被删除。", "删除成员", {
const ok = await ElMessageBox.confirm(TEXT.modules.adminProjectMembers.removeConfirm, TEXT.modules.adminProjectMembers.removeTitle, {
type: "warning",
confirmButtonText: "确认",
cancelButtonText: "取消",
confirmButtonText: TEXT.common.actions.confirm,
cancelButtonText: TEXT.common.actions.cancel,
}).catch(() => null);
if (!ok) return;
try {
await removeMember(projectId.value, row.id);
members.value = members.value.filter((m) => m.id !== row.id);
ElMessage.success("已从项目移除");
ElMessage.success(TEXT.modules.adminProjectMembers.removeSuccess);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
@@ -369,7 +370,7 @@ const onDelete = async (row: StudyMember) => {
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "删除失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
logAudit("PROJECT_MEMBER_UPDATED", {
targetId: projectId.value,
targetName: project.value?.name,
+16 -19
View File
@@ -3,37 +3,37 @@
<el-card>
<div class="header">
<div>
<h3>项目治理</h3>
<p class="sub">项目是独立业务实体账号加入项目后才成为项目成员角色在成员配置中设置</p>
<h3>{{ TEXT.modules.adminProjects.title }}</h3>
<p class="sub">{{ TEXT.modules.adminProjects.subtitle }}</p>
</div>
<el-button type="primary" @click="openCreate">新建项目</el-button>
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjects.projectLabel }}</el-button>
</div>
<el-table :data="projects" v-loading="loading" stripe>
<el-table-column prop="code" label="项目编号" width="140">
<el-table-column prop="code" :label="TEXT.modules.adminProjects.projectCode" width="140">
<template #default="scope">
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.code }}</el-link>
</template>
</el-table-column>
<el-table-column prop="name" label="项目名称" min-width="200">
<el-table-column prop="name" :label="TEXT.common.fields.projectName" min-width="200">
<template #default="scope">
<el-link type="primary" @click="goDetail(scope.row)">{{ scope.row.name }}</el-link>
</template>
</el-table-column>
<el-table-column prop="sponsor" label="申办方" min-width="160" />
<el-table-column prop="status" label="状态" width="120">
<el-table-column prop="sponsor" :label="TEXT.common.fields.sponsor" min-width="160" />
<el-table-column prop="status" :label="TEXT.common.fields.status" 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 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="操作" width="320" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" 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>
<el-button link type="primary" size="small" @click="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
<el-button link type="primary" size="small" @click="goMembers(scope.row)">{{ TEXT.modules.adminProjects.members }}</el-button>
<el-button link type="primary" size="small" @click="goSites(scope.row)">{{ TEXT.modules.adminProjects.sites }}</el-button>
<el-button link type="success" size="small" @click="enterStudy(scope.row)">{{ TEXT.modules.adminProjects.enter }}</el-button>
</template>
</el-table-column>
</el-table>
@@ -51,6 +51,7 @@ import type { Study } from "../../types/api";
import ProjectForm from "./ProjectForm.vue";
import { useStudyStore } from "../../store/study";
import { displayDateTime } from "../../utils/display";
import { TEXT } from "../../locales";
const projects = ref<Study[]>([]);
const loading = ref(false);
@@ -66,7 +67,7 @@ const loadProjects = async () => {
const items = (data as any).items || [];
projects.value = items;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "项目列表加载失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminProjects.loadFailed);
} finally {
loading.value = false;
}
@@ -100,11 +101,7 @@ const goDetail = (row: Study) => {
};
const statusLabel = (status: string) =>
({
DRAFT: "草稿",
ACTIVE: "进行中",
CLOSED: "已关闭",
}[status] || status);
TEXT.enums.projectStatus[status as keyof typeof TEXT.enums.projectStatus] || status;
const statusTag = (status: string) =>
({
+11 -10
View File
@@ -1,17 +1,17 @@
<template>
<el-dialog title="CRA 绑定" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
<div class="tip">中心{{ site?.name }}</div>
<el-dialog :title="TEXT.modules.adminSites.craBindTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
<div class="tip">{{ TEXT.modules.adminSites.sitePrefix }}{{ 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-form-item :label="TEXT.modules.adminSites.craSelect">
<el-select v-model="selectedCras" multiple filterable :placeholder="TEXT.modules.adminSites.craSelectPlaceholder" 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>
<div class="hint">{{ TEXT.modules.adminSites.craHint }}</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>
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="saving" @click="onSave">{{ TEXT.modules.adminSites.craSave }}</el-button>
</template>
</el-dialog>
</template>
@@ -24,6 +24,7 @@ import type { Site, UserInfo } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { TEXT } from "../../locales";
const props = defineProps<{
visible: boolean;
@@ -83,7 +84,7 @@ const onSave = async () => {
target: { siteId: props.site.id, studyId: props.studyId },
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || "无权限执行该操作");
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: props.site.id,
targetName: props.site.name,
@@ -98,7 +99,7 @@ const onSave = async () => {
.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("绑定已保存");
ElMessage.success(TEXT.modules.adminSites.craSaveSuccess);
emit("saved");
visibleProxy.value = false;
logAudit("SITE_CRA_BOUND", {
@@ -109,7 +110,7 @@ const onSave = async () => {
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "保存失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
logAudit("SITE_CRA_BOUND", {
targetId: props.site.id,
targetName: props.site.name,
+21 -20
View File
@@ -1,24 +1,24 @@
<template>
<el-dialog :title="site ? '编辑中心' : '新建中心'" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
<el-dialog :title="site ? TEXT.modules.adminSites.editTitle : TEXT.modules.adminSites.newTitle" 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 :label="TEXT.common.fields.siteName" prop="name">
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.siteName" />
</el-form-item>
<el-form-item label="城市" prop="city">
<el-input v-model="form.city" placeholder="所在城市(可选)" />
<el-form-item :label="TEXT.common.fields.city" prop="city">
<el-input v-model="form.city" :placeholder="TEXT.modules.adminSites.cityPlaceholder" />
</el-form-item>
<el-form-item label="PI" prop="pi_name">
<el-input v-model="form.pi_name" placeholder="PI(可选)" />
<el-form-item :label="TEXT.modules.adminSites.piLabel" prop="pi_name">
<el-input v-model="form.pi_name" :placeholder="TEXT.modules.adminSites.piPlaceholder" />
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="form.phone" placeholder="电话(可选)" />
<el-form-item :label="TEXT.common.fields.phone" prop="phone">
<el-input v-model="form.phone" :placeholder="TEXT.modules.adminSites.phonePlaceholder" />
</el-form-item>
<el-form-item label="负责人" prop="contact">
<el-form-item :label="TEXT.common.fields.contact" prop="contact">
<el-select
v-model="form.craSelections"
multiple
filterable
placeholder="选择项目成员作为负责人,可多选"
:placeholder="TEXT.modules.adminSites.contactPlaceholder"
>
<el-option
v-for="user in craOptions"
@@ -28,13 +28,13 @@
/>
</el-select>
</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 v-if="site" :label="TEXT.common.fields.status" prop="is_active">
<el-switch v-model="form.is_active" :active-text="TEXT.common.actions.enable" :inactive-text="TEXT.common.actions.disable" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visibleProxy = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
</template>
</el-dialog>
</template>
@@ -47,6 +47,7 @@ import type { Site } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { TEXT, requiredMessage } from "../../locales";
const props = defineProps<{
visible: boolean;
@@ -80,7 +81,7 @@ const form = reactive({
});
const rules = reactive<FormRules>({
name: [{ required: true, message: "请输入中心名称", trigger: "blur" }],
name: [{ required: true, message: requiredMessage(TEXT.common.fields.siteName), trigger: "blur" }],
});
const craOptions = computed(() => {
@@ -138,7 +139,7 @@ const onSubmit = async () => {
target: { siteId: props.site?.id, studyId: props.studyId },
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || "无权限执行该操作");
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: props.site?.id || props.studyId,
targetName: props.site?.name,
@@ -159,7 +160,7 @@ const onSubmit = async () => {
contact,
is_active: form.is_active,
});
ElMessage.success("中心已更新");
ElMessage.success(TEXT.modules.adminSites.updateSuccess);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.site.id,
targetName: props.site.name,
@@ -173,7 +174,7 @@ const onSubmit = async () => {
pi_name: form.pi_name,
contact,
});
ElMessage.success("中心已创建");
ElMessage.success(TEXT.modules.adminSites.createSuccess);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.studyId,
targetName: form.name,
@@ -184,7 +185,7 @@ const onSubmit = async () => {
emit("saved");
visibleProxy.value = false;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "保存失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.site?.id || props.studyId,
targetName: props.site?.name || form.name,
+24 -23
View File
@@ -3,41 +3,41 @@
<el-card>
<div class="header">
<div>
<h3>中心管理</h3>
<div class="sub" v-if="project">项目{{ project.code }} - {{ project.name }}</div>
<h3>{{ TEXT.modules.adminSites.title }}</h3>
<div class="sub" v-if="project">{{ TEXT.modules.adminSites.projectPrefix }}{{ project.code }} - {{ project.name }}</div>
</div>
<el-button type="primary" @click="openCreate">新建中心</el-button>
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminSites.siteLabel }}</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 label="电话" width="160">
<el-table-column prop="name" :label="TEXT.common.fields.siteName" min-width="180" />
<el-table-column prop="city" :label="TEXT.common.fields.city" width="140" />
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" width="160" />
<el-table-column :label="TEXT.common.fields.phone" width="160">
<template #default="scope">
{{ phoneText(scope.row) }}
</template>
</el-table-column>
<el-table-column prop="contact" label="负责人" min-width="180">
<el-table-column prop="contact" :label="TEXT.common.fields.contact" min-width="180">
<template #default="scope">
{{ contactLabel(scope.row) }}
</template>
</el-table-column>
<el-table-column label="状态" width="120">
<el-table-column :label="TEXT.common.fields.status" 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>
<el-tag type="success" v-if="scope.row.is_active">{{ TEXT.common.actions.enable }}</el-tag>
<el-tag type="danger" v-else>{{ TEXT.common.actions.disable }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" width="220" 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="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
<el-button
link
:type="scope.row.is_active ? 'danger' : 'primary'"
size="small"
@click="toggleSite(scope.row)"
>
{{ scope.row.is_active ? "停用" : "启用" }}
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
</el-button>
</template>
</el-table-column>
@@ -68,6 +68,7 @@ import type { Site, Study, UserInfo } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { TEXT } from "../../locales";
const route = useRoute();
const projectId = computed(() => route.params.projectId as string);
@@ -98,7 +99,7 @@ const loadSites = async () => {
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 || "中心列表加载失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminSites.loadFailed);
} finally {
loading.value = false;
}
@@ -135,16 +136,16 @@ const memberNameMap = computed(() => {
});
const contactLabel = (row: any) => {
if (!row?.contact) return "—";
if (!row?.contact) return TEXT.common.fallback;
return String(row.contact)
.split(",")
.map((c) => c.trim())
.filter(Boolean)
.map((c) => memberNameMap.value[c] || c)
.join("、") || "—";
.join("、") || TEXT.common.fallback;
};
const phoneText = (row: any) => row?.phone || row?.contact_phone || "—";
const phoneText = (row: any) => row?.phone || row?.contact_phone || TEXT.common.fallback;
const openCreate = () => {
loadUsers();
@@ -168,7 +169,7 @@ const toggleSite = async (row: Site) => {
target: { siteId: row.id, studyId: projectId.value },
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || "无权限执行该操作");
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: row.id,
targetName: row.name,
@@ -178,15 +179,15 @@ const toggleSite = async (row: Site) => {
return;
}
if (row.is_active) {
const ok = await ElMessageBox.confirm("该操作将影响中心数据,请确认是否继续", "停用中心", {
const ok = await ElMessageBox.confirm(TEXT.modules.adminSites.disableConfirm, TEXT.modules.adminSites.disableTitle, {
type: "warning",
confirmButtonText: "确认",
confirmButtonText: TEXT.common.actions.confirm,
}).catch(() => null);
if (!ok) return;
}
try {
await updateSite(projectId.value, row.id, { is_active: !row.is_active });
ElMessage.success(row.is_active ? "已停用" : "已启用");
ElMessage.success(row.is_active ? TEXT.modules.adminSites.disableSuccess : TEXT.modules.adminSites.enableSuccess);
loadSites();
logAudit("SITE_STATUS_CHANGED", {
targetId: row.id,
@@ -196,7 +197,7 @@ const toggleSite = async (row: Site) => {
severity: "normal",
});
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "操作失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
logAudit("SITE_STATUS_CHANGED", {
targetId: row.id,
targetName: row.name,
+23 -22
View File
@@ -1,25 +1,25 @@
<template>
<el-dialog :title="user ? '编辑用户' : '新建用户'" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
<el-dialog :title="user ? TEXT.modules.adminUsers.editTitle : TEXT.modules.adminUsers.newTitle" 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="email">
<el-input v-model="form.email" :disabled="!!user" placeholder="请输入邮箱" />
<el-form-item :label="TEXT.common.fields.email" prop="email">
<el-input v-model="form.email" :disabled="!!user" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.email" />
</el-form-item>
<el-form-item label="姓名" prop="full_name">
<el-input v-model="form.full_name" placeholder="请输入姓名" />
<el-form-item :label="TEXT.common.fields.name" prop="full_name">
<el-input v-model="form.full_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
</el-form-item>
<el-form-item label="部门" prop="department">
<el-input v-model="form.department" placeholder="请输入部门" />
<el-form-item :label="TEXT.common.fields.department" prop="department">
<el-input v-model="form.department" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.department" />
</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 :label="user ? TEXT.modules.adminUsers.passwordOptional : TEXT.modules.adminUsers.passwordInitial" prop="password">
<el-input v-model="form.password" type="password" show-password :placeholder="user ? TEXT.modules.adminUsers.passwordOptionalHint : TEXT.modules.adminUsers.passwordInitialHint" />
</el-form-item>
<el-form-item v-if="user" label="账号状态" prop="is_active">
<el-switch v-model="form.is_active" active-text="启用" inactive-text="禁用" :disabled="isLastAdmin" />
<el-form-item v-if="user" :label="TEXT.modules.adminUsers.accountStatus" prop="is_active">
<el-switch v-model="form.is_active" :active-text="TEXT.common.actions.enable" :inactive-text="TEXT.common.actions.disable" :disabled="isLastAdmin" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visibleProxy = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">保存</el-button>
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
</template>
</el-dialog>
</template>
@@ -29,6 +29,7 @@ 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";
import { TEXT, requiredMessage } from "../../locales";
const props = defineProps<{
visible: boolean;
@@ -60,16 +61,16 @@ const defaultRole = "PV";
const rules = reactive<FormRules>({
email: [
{ required: true, message: "请输入邮箱", trigger: "blur" },
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
{ required: true, message: requiredMessage(TEXT.common.fields.email), trigger: "blur" },
{ type: "email", message: TEXT.common.validation.invalidEmail, trigger: "blur" },
],
full_name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
department: [{ required: true, message: "请输入部门", trigger: "blur" }],
full_name: [{ required: true, message: requiredMessage(TEXT.common.fields.name), trigger: "blur" }],
department: [{ required: true, message: requiredMessage(TEXT.common.fields.department), trigger: "blur" }],
password: [
{
validator: (_rule, value, callback) => {
if (!props.user && !value) {
callback(new Error("请输入初始密码"));
callback(new Error(TEXT.modules.adminUsers.passwordInitialRequired));
return;
}
callback();
@@ -113,7 +114,7 @@ const onSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate();
if (props.user?.role === "ADMIN" && (props.adminCount || 0) <= 1 && !form.is_active) {
ElMessage.warning("至少保留一个管理员账号,不能禁用该账号");
ElMessage.warning(TEXT.modules.adminUsers.keepAdminWarning);
return;
}
submitting.value = true;
@@ -125,7 +126,7 @@ const onSubmit = async () => {
department: form.department,
password: form.password || undefined,
});
ElMessage.success("用户已更新");
ElMessage.success(TEXT.modules.adminUsers.updateSuccess);
} else {
await createUser({
email: form.email,
@@ -134,12 +135,12 @@ const onSubmit = async () => {
role: defaultRole,
password: form.password,
});
ElMessage.success("用户已创建");
ElMessage.success(TEXT.modules.adminUsers.createSuccess);
}
emit("saved");
visibleProxy.value = false;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "保存失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
} finally {
submitting.value = false;
}
+27 -26
View File
@@ -1,24 +1,24 @@
<template>
<el-dialog title="重置密码" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
<el-dialog :title="TEXT.modules.adminUsers.resetTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
<el-alert
type="warning"
show-icon
title="该操作将影响用户账号,请确认是否继续"
description="系统将生成临时密码,需再次输入用户名才能提交。"
:title="TEXT.modules.adminUsers.resetWarningTitle"
:description="TEXT.modules.adminUsers.resetWarningDesc"
class="mb-12"
/>
<div class="info">重置对象<strong>{{ user?.username }}</strong></div>
<div class="info">{{ TEXT.modules.adminUsers.resetTarget }}<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 :label="TEXT.modules.adminUsers.confirmUsername" prop="confirmUsername">
<el-input v-model="form.confirmUsername" :placeholder="TEXT.modules.adminUsers.confirmUsernameHint" />
</el-form-item>
<el-form-item label="密码方式" prop="mode">
<el-form-item :label="TEXT.modules.adminUsers.passwordMode" prop="mode">
<el-radio-group v-model="form.mode">
<el-radio label="auto">系统生成临时密码</el-radio>
<el-radio label="manual">手动输入新密码</el-radio>
<el-radio label="auto">{{ TEXT.modules.adminUsers.passwordAuto }}</el-radio>
<el-radio label="manual">{{ TEXT.modules.adminUsers.passwordManual }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="form.mode === 'auto' ? '临时密码' : '新密码'" prop="passwordValue">
<el-form-item :label="form.mode === 'auto' ? TEXT.modules.adminUsers.tempPassword : TEXT.modules.adminUsers.newPassword" prop="passwordValue">
<el-input
v-if="form.mode === 'auto'"
v-model="form.tempPassword"
@@ -27,7 +27,7 @@
readonly
>
<template #append>
<el-button @click="regenerate">重新生成</el-button>
<el-button @click="regenerate">{{ TEXT.modules.adminUsers.regenerate }}</el-button>
</template>
</el-input>
<el-input
@@ -35,14 +35,14 @@
v-model="form.manualPassword"
type="password"
show-password
placeholder="请输入新密码"
:placeholder="TEXT.modules.adminUsers.manualPasswordHint"
/>
<div class="hint">不会明文展示请通过安全渠道告知用户</div>
<div class="hint">{{ TEXT.modules.adminUsers.passwordHint }}</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>
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="danger" :disabled="!canSubmit" :loading="submitting" @click="onSubmit">{{ TEXT.modules.adminUsers.resetConfirm }}</el-button>
</template>
</el-dialog>
</template>
@@ -52,6 +52,7 @@ 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";
import { TEXT, requiredMessage } from "../../locales";
const props = defineProps<{
visible: boolean;
@@ -78,17 +79,17 @@ const form = reactive({
});
const rules = reactive<FormRules>({
confirmUsername: [{ required: true, message: "请再次输入用户名", trigger: "blur" }],
mode: [{ required: true, message: "请选择密码方式", trigger: "change" }],
confirmUsername: [{ required: true, message: requiredMessage(TEXT.modules.adminUsers.confirmUsername), trigger: "blur" }],
mode: [{ required: true, message: requiredMessage(TEXT.modules.adminUsers.passwordMode), trigger: "change" }],
passwordValue: [
{
validator: (_rule, _value, callback) => {
if (form.mode === "auto" && !form.tempPassword) {
callback(new Error("临时密码生成异常"));
callback(new Error(TEXT.modules.adminUsers.tempPasswordError));
return;
}
if (form.mode === "manual" && !form.manualPassword) {
callback(new Error("请输入新密码"));
callback(new Error(TEXT.modules.adminUsers.manualPasswordRequired));
return;
}
callback();
@@ -127,16 +128,16 @@ const onSubmit = async () => {
if (!props.user) return;
await formRef.value?.validate();
if (form.confirmUsername !== props.user.username) {
ElMessage.error("确认用户名不匹配,无法提交");
ElMessage.error(TEXT.modules.adminUsers.confirmMismatch);
return;
}
const ok = await ElMessageBox.confirm(
"该操作将影响用户账号,请确认是否继续",
"确认重置密码",
TEXT.modules.adminUsers.resetConfirmPrompt,
TEXT.modules.adminUsers.resetConfirmTitle,
{
type: "warning",
confirmButtonText: "确认重置",
cancelButtonText: "取消",
confirmButtonText: TEXT.modules.adminUsers.resetConfirm,
cancelButtonText: TEXT.common.actions.cancel,
}
).catch(() => null);
if (!ok) return;
@@ -144,11 +145,11 @@ const onSubmit = async () => {
try {
const password = form.mode === "manual" ? form.manualPassword : form.tempPassword;
await updateUser(props.user.id, { password });
ElMessage.success("密码已重置");
ElMessage.success(TEXT.modules.adminUsers.resetSuccess);
emit("reset");
visibleProxy.value = false;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "重置失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminUsers.resetFailed);
} finally {
submitting.value = false;
}
+35 -34
View File
@@ -3,28 +3,28 @@
<el-card>
<div class="header">
<div>
<h3>账号治理系统登录账号</h3>
<p class="sub">账号用于登录系统本身不具备项目或角色权限</p>
<h3>{{ TEXT.modules.adminUsers.title }}</h3>
<p class="sub">{{ TEXT.modules.adminUsers.subtitle }}</p>
</div>
<el-button type="primary" @click="openCreate">新建用户</el-button>
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminUsers.userLabel }}</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">
<el-table-column prop="email" :label="TEXT.common.fields.email" min-width="200" />
<el-table-column prop="full_name" :label="TEXT.common.fields.name" min-width="140" />
<el-table-column prop="department" :label="TEXT.common.fields.department" min-width="140" />
<el-table-column :label="TEXT.common.fields.status" 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">
<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="操作" width="320" fixed="right">
<el-table-column :label="TEXT.common.labels.actions" 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="openEdit(scope.row)">{{ TEXT.common.actions.edit }}</el-button>
<el-button
link
:type="scope.row.status === 'ACTIVE' ? 'danger' : 'primary'"
@@ -32,9 +32,9 @@
:disabled="isLastAdmin(scope.row)"
@click="toggleStatus(scope.row)"
>
{{ scope.row.status === 'ACTIVE' ? "禁用" : "启用" }}
{{ 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)">重置密码</el-button>
<el-button link type="danger" size="small" @click="openReset(scope.row)">{{ TEXT.modules.adminUsers.resetPassword }}</el-button>
<el-button
link
type="danger"
@@ -42,7 +42,7 @@
:disabled="scope.row.id === auth.user?.id"
@click="onDelete(scope.row)"
>
删除
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
@@ -72,6 +72,7 @@ import UserForm from "./UserForm.vue";
import UserResetPassword from "./UserResetPassword.vue";
import { useAuthStore } from "../../store/auth";
import { displayDateTime } from "../../utils/display";
import { TEXT } from "../../locales";
const users = ref<UserInfo[]>([]);
const total = ref(0);
@@ -98,7 +99,7 @@ const loadUsers = async () => {
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 || "用户列表加载失败");
ElMessage.error(e?.response?.data?.message || TEXT.modules.adminUsers.loadFailed);
} finally {
loading.value = false;
}
@@ -116,26 +117,26 @@ const openEdit = (row: UserInfo) => {
const toggleStatus = async (row: UserInfo) => {
if (isLastAdmin(row)) {
ElMessage.warning("至少保留一个管理员账号,不能禁用该账号");
ElMessage.warning(TEXT.modules.adminUsers.keepAdminWarning);
return;
}
const disable = row.status === "ACTIVE";
const ok = await ElMessageBox.confirm(
disable ? "该操作将影响用户账号,请确认是否继续" : "确认启用该用户账号?",
disable ? "禁用账号" : "启用账号",
disable ? TEXT.modules.adminUsers.disableConfirm : TEXT.modules.adminUsers.enableConfirm,
disable ? TEXT.modules.adminUsers.disableTitle : TEXT.modules.adminUsers.enableTitle,
{
type: disable ? "warning" : "info",
confirmButtonText: "确认",
cancelButtonText: "取消",
confirmButtonText: TEXT.common.actions.confirm,
cancelButtonText: TEXT.common.actions.cancel,
}
).catch(() => null);
if (!ok) return;
try {
await updateUser(row.id, { status: disable ? "DISABLED" : "ACTIVE", is_active: !disable });
ElMessage.success(disable ? "已禁用" : "已启用");
ElMessage.success(disable ? TEXT.modules.adminUsers.disableSuccess : TEXT.modules.adminUsers.enableSuccess);
loadUsers();
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || "操作失败");
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.actionFailed);
}
};
@@ -146,28 +147,28 @@ const openReset = (row: UserInfo) => {
const onDelete = async (row: UserInfo) => {
const { value, action } = await ElMessageBox.prompt(
"删除后账号将无法登录且无法恢复,需先在各项目成员配置中移除该账号。请输入当前管理员密码确认删除。",
"危险操作:删除账号",
TEXT.modules.adminUsers.deleteConfirm,
TEXT.modules.adminUsers.deleteTitle,
{
inputType: "password",
inputPlaceholder: "请输入 admin 密码",
confirmButtonText: "确认删除",
inputPlaceholder: TEXT.modules.adminUsers.deletePasswordPlaceholder,
confirmButtonText: TEXT.modules.adminUsers.deleteConfirmButton,
confirmButtonClass: "el-button--danger",
cancelButtonText: "取消",
cancelButtonText: TEXT.common.actions.cancel,
}
).catch(() => ({ action: "cancel", value: "" }));
if (action !== "confirm" || !value) return;
try {
await deleteUser(row.id, { admin_password: value });
ElMessage.success("账号已删除");
ElMessage.success(TEXT.modules.adminUsers.deleteSuccess);
loadUsers();
} catch (e: any) {
const detail = e?.response?.data?.detail || e?.response?.data?.message;
if (detail && detail.includes("项目成员")) {
if (detail && detail.includes(TEXT.modules.adminProjectMembers.memberLabel)) {
ElMessage.closeAll();
ElMessage.error(detail);
} else {
ElMessage.error(detail || "删除失败");
ElMessage.error(detail || TEXT.modules.adminUsers.deleteFailed);
}
}
};
@@ -192,15 +193,15 @@ const statusType = (status: string) => {
const statusLabel = (status: string) => {
switch (status) {
case "ACTIVE":
return "启用";
return TEXT.enums.userStatus.ACTIVE;
case "PENDING":
return "待审核";
return TEXT.enums.userStatus.PENDING;
case "REJECTED":
return "已拒绝";
return TEXT.enums.userStatus.REJECTED;
case "DISABLED":
return "已停用";
return TEXT.enums.userStatus.DISABLED;
default:
return status || "未知";
return status || TEXT.common.fallback;
}
};