统一项目角色展示与预设文案
This commit is contained in:
@@ -8,6 +8,15 @@ import type { ApiEndpointPermissionsResponse } from "@/types/api";
|
||||
|
||||
vi.mock("@/api/projectPermissions", () => ({
|
||||
fetchApiOperations: vi.fn().mockResolvedValue({ data: { operations: [] } }),
|
||||
fetchPermissionTemplates: vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
{ category: "PM", name: "PM", description: "项目负责人,统筹项目全局,协调进度、资源与关键决策。" },
|
||||
{ category: "CRA", name: "CRA", description: "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。" },
|
||||
{ category: "IMP", name: "CTA", description: "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。" },
|
||||
{ category: "MEDICAL_REVIEW", name: "QA", description: "负责医学审核与稽查,关注质量风险、合规性和医学一致性。" },
|
||||
{ category: "PV", name: "PV", description: "负责药物警戒相关工作,跟踪安全性事件并支持风险评估。" },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("ApiEndpointPermissions.vue", () => {
|
||||
@@ -70,6 +79,17 @@ describe("ApiEndpointPermissions.vue", () => {
|
||||
expect(source).toContain("updatedMatrix[role][operation_key] = allowed");
|
||||
});
|
||||
|
||||
it("renders role headers from template display names while keeping matrix role keys", () => {
|
||||
const source = readFileSync(resolve(__dirname, "./ApiEndpointPermissions.vue"), "utf8");
|
||||
|
||||
expect(source).toContain("useRoleTemplateMeta");
|
||||
expect(source).toContain(':label="roleLabel(role)"');
|
||||
expect(source).toContain(':key="role"');
|
||||
expect(source).toContain("isOperationAllowed(row.operation_key, role)");
|
||||
expect(source).toContain("onPermissionChange(row.operation_key, role, val)");
|
||||
expect(source).not.toContain(':label="role"');
|
||||
});
|
||||
|
||||
it("filters endpoints by search text", async () => {
|
||||
const wrapper = mount(ApiEndpointPermissions, {
|
||||
props: {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<el-table-column
|
||||
v-for="role in roles"
|
||||
:key="role"
|
||||
:label="role"
|
||||
:label="roleLabel(role)"
|
||||
:width="90"
|
||||
align="center"
|
||||
>
|
||||
@@ -81,6 +81,7 @@ import { fetchApiOperations } from "@/api/projectPermissions";
|
||||
import { isApiPermissionAllowed } from "@/utils/apiPermissionValue";
|
||||
import { useAuthStore } from "@/store/auth";
|
||||
import { isSystemAdmin } from "@/utils/roles";
|
||||
import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta";
|
||||
|
||||
interface Operation {
|
||||
operation_key: string;
|
||||
@@ -103,6 +104,7 @@ const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const auth = useAuthStore();
|
||||
const { roleLabel, loadRoleTemplates } = useRoleTemplateMeta();
|
||||
const isAdmin = computed(() => isSystemAdmin(auth.user));
|
||||
const isRoleEditable = (role: string) => {
|
||||
if (role === "ADMIN") return false;
|
||||
@@ -267,6 +269,7 @@ const onPermissionChange = (operation_key: string, role: string, allowed: boolea
|
||||
|
||||
onMounted(() => {
|
||||
loadOperations();
|
||||
loadRoleTemplates();
|
||||
});
|
||||
|
||||
defineExpose({ searchText });
|
||||
|
||||
@@ -36,6 +36,18 @@ describe("PermissionAccessLogs", () => {
|
||||
expect(source).not.toContain('QA: "QA"');
|
||||
});
|
||||
|
||||
it("uses permission template names for role filters and log lines", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain("useRoleTemplateMeta");
|
||||
expect(source).toContain("const { roleLabel, roleOptionsFor, loadRoleTemplates } = useRoleTemplateMeta();");
|
||||
expect(source).toContain("const roleFilterOptions = computed(() => roleOptionsFor(ROLE_FILTER_KEYS));");
|
||||
expect(source).toContain('v-for="role in roleFilterOptions"');
|
||||
expect(source).toContain("role=${roleLabel(row.role)}");
|
||||
expect(source).not.toContain("const ROLE_LABELS");
|
||||
expect(source).not.toContain('label="医学审核" value="MEDICAL_REVIEW"');
|
||||
});
|
||||
|
||||
it("uses narrow metric cards without helper subtitles", () => {
|
||||
const source = readSource();
|
||||
|
||||
|
||||
@@ -12,12 +12,7 @@
|
||||
@change="onFilterChange"
|
||||
/>
|
||||
<el-select v-model="filters.role" placeholder="角色" clearable style="width: 130px" @change="onFilterChange">
|
||||
<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="医学审核" value="MEDICAL_REVIEW" />
|
||||
<el-option label="IMP" value="IMP" />
|
||||
<el-option v-for="role in roleFilterOptions" :key="role.value" :label="role.label" :value="role.value" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.allowed" placeholder="结果" clearable style="width: 100px" @change="onFilterChange">
|
||||
<el-option label="允许" :value="true" />
|
||||
@@ -157,11 +152,13 @@ import type {
|
||||
SecurityAccessLogsResponse,
|
||||
} from "@/types/api";
|
||||
import { Search as SearchIcon } from "@element-plus/icons-vue";
|
||||
import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta";
|
||||
|
||||
const props = withDefaults(defineProps<{ showSecurityLog?: boolean }>(), {
|
||||
showSecurityLog: false,
|
||||
});
|
||||
const showSecurityLog = computed(() => props.showSecurityLog);
|
||||
const { roleLabel, roleOptionsFor, loadRoleTemplates } = useRoleTemplateMeta();
|
||||
|
||||
const MetricIconVisit = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [
|
||||
h("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
|
||||
@@ -222,14 +219,8 @@ const filters = reactive({
|
||||
allowed: undefined as boolean | undefined,
|
||||
});
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
ADMIN: "管理员",
|
||||
PM: "项目负责人",
|
||||
CRA: "CRA",
|
||||
PV: "PV",
|
||||
MEDICAL_REVIEW: "医学审核",
|
||||
IMP: "药品管理",
|
||||
};
|
||||
const ROLE_FILTER_KEYS = ["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"];
|
||||
const roleFilterOptions = computed(() => roleOptionsFor(ROLE_FILTER_KEYS));
|
||||
|
||||
const SECURITY_AUTH_LABELS: Record<string, string> = {
|
||||
ANONYMOUS: "匿名",
|
||||
@@ -243,8 +234,6 @@ const formatTerminalTime = (iso: string) => {
|
||||
return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
};
|
||||
|
||||
const roleLabel = (role: string) => ROLE_LABELS[role] || role;
|
||||
|
||||
const formatIpLocation = (row: AccessLogItem) => {
|
||||
const parts = [row.ip_province, row.ip_city, row.ip_isp].filter(Boolean);
|
||||
return parts.length ? parts.join(" / ") : row.ip_location;
|
||||
@@ -437,7 +426,8 @@ const downloadSecurityLog = () => {
|
||||
downloadLogFile("security-event-audit.log", securityTerminalLines.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
await loadRoleTemplates();
|
||||
refresh();
|
||||
startRealtimePolling();
|
||||
});
|
||||
|
||||
@@ -20,6 +20,10 @@ describe("PermissionTemplateSelector", () => {
|
||||
it("reads current project permissions by role key, not by role label", () => {
|
||||
const source = readSource();
|
||||
|
||||
expect(source).toContain('<span class="card-name">{{ template.name }}</span>');
|
||||
expect(source).toContain("refreshKey?: number;");
|
||||
expect(source).toContain("watch(() => props.refreshKey, loadTemplates);");
|
||||
expect(source).not.toContain("ROLE_LABELS");
|
||||
expect(source).toContain("template.category && props.currentPermissions[template.category]");
|
||||
expect(source).toContain("enabledPercent(template.category)");
|
||||
expect(source).toContain("countCurrentEnabled(template.category)");
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="card-top">
|
||||
<span class="card-icon">{{ roleIcon(template.category) }}</span>
|
||||
<div class="card-title-wrap">
|
||||
<span class="card-name">{{ template.category ? (ROLE_LABELS[template.category] || template.name) : template.name }}</span>
|
||||
<span class="card-name">{{ template.name }}</span>
|
||||
<span v-if="template.description" class="card-desc">{{ template.description }}</span>
|
||||
</div>
|
||||
<el-tag v-if="!template.is_system" size="small" effect="dark" round type="success" class="card-badge">自定义</el-tag>
|
||||
@@ -62,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
fetchPermissionTemplates,
|
||||
@@ -73,6 +73,7 @@ const props = defineProps<{
|
||||
studyId: string;
|
||||
currentPermissions?: Record<string, Record<string, boolean>>;
|
||||
activeRoles?: string[];
|
||||
refreshKey?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -81,11 +82,6 @@ const emit = defineEmits<{
|
||||
|
||||
const templates = ref<PermissionTemplate[]>([]);
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
PM: "项目负责人", CRA: "CRA", PV: "PV",
|
||||
MEDICAL_REVIEW: "医学审核", IMP: "药品管理员",
|
||||
};
|
||||
|
||||
const roleIcon = (category: string | null) => {
|
||||
const icons: Record<string, string> = {
|
||||
PM: "👑", CRA: "📋", PV: "🔍",
|
||||
@@ -144,6 +140,7 @@ const loadTemplates = async () => {
|
||||
};
|
||||
|
||||
onMounted(loadTemplates);
|
||||
watch(() => props.refreshKey, loadTemplates);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user