立项配置页初步优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card shadow="never" class="main-content-card">
|
||||
<div class="filter-container">
|
||||
<el-card shadow="never" class="main-content-card unified-shell">
|
||||
<div class="filter-container unified-action-bar">
|
||||
<div class="filter-form">
|
||||
<el-tag type="info" effect="plain" class="filter-label-tag">{{ TEXT.common.fields.status }}</el-tag>
|
||||
<el-select v-model="status" style="width: 140px" @change="loadUsers" class="filter-select-comp">
|
||||
@@ -13,45 +13,47 @@
|
||||
<div class="filter-spacer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="users" v-loading="loading" style="width: 100%">
|
||||
<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="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusType(scope.row.status)">{{ displayEnum(TEXT.enums.userStatus, scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="220" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
type="success"
|
||||
link
|
||||
size="small"
|
||||
@click="onApprove(scope.row.id)"
|
||||
>
|
||||
{{ TEXT.modules.adminUserApproval.approve }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="onReject(scope.row.id)"
|
||||
>
|
||||
{{ TEXT.modules.adminUserApproval.reject }}
|
||||
</el-button>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="unified-section approval-table-section">
|
||||
<el-table :data="users" v-loading="loading" style="width: 100%">
|
||||
<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="TEXT.common.fields.status" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusType(scope.row.status)">{{ displayEnum(TEXT.enums.userStatus, scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="220" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
type="success"
|
||||
link
|
||||
size="small"
|
||||
@click="onApprove(scope.row.id)"
|
||||
>
|
||||
{{ TEXT.modules.adminUserApproval.approve }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'PENDING'"
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="onReject(scope.row.id)"
|
||||
>
|
||||
{{ TEXT.modules.adminUserApproval.reject }}
|
||||
</el-button>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -124,14 +126,6 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -150,4 +144,9 @@ onMounted(() => {
|
||||
.filter-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.approval-table-section {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card shadow="never" class="main-content-card">
|
||||
<div class="filter-container">
|
||||
<el-card shadow="never" class="main-content-card unified-shell">
|
||||
<div class="filter-container unified-action-bar">
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<el-form-item label="" class="filter-item-form">
|
||||
<el-select v-model="filters.entityType" clearable :placeholder="TEXT.modules.adminAuditLogs.filterEntityType" @change="loadLogs" class="filter-select-comp">
|
||||
<el-option v-for="opt in entityTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="" class="filter-item-form">
|
||||
<el-select v-model="filters.eventType" clearable :placeholder="TEXT.modules.adminAuditLogs.filterEvent" @change="loadLogs" class="filter-select-comp">
|
||||
<el-option v-for="opt in eventTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
@@ -43,53 +48,55 @@
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="logs" v-loading="loading" style="width: 100%" stripe>
|
||||
<el-table-column prop="timestamp" :label="TEXT.modules.adminAuditLogs.columns.time" width="180" align="center">
|
||||
<template #default="scope">
|
||||
<span class="text-gray-500">{{ displayDateTime(scope.row.timestamp) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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" width="160">
|
||||
<template #default="scope">
|
||||
<el-tag effect="plain">{{ scope.row.eventLabel }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<strong>{{ scope.row.targetTypeLabel }}</strong>
|
||||
<span class="text-gray-500" v-if="scope.row.targetName">({{ scope.row.targetName }})</span>
|
||||
</span>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.diff" min-width="260">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.diffText?.length" class="diff-container">
|
||||
<div v-for="(line, idx) in scope.row.diffText" :key="idx" class="diff-line">{{ line }}</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">{{ TEXT.audit.emptyValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.result === 'SUCCESS' ? 'success' : 'danger'" effect="dark" size="small">
|
||||
{{ scope.row.resultLabel }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
layout="prev, pager, next"
|
||||
:page-size="pageSize"
|
||||
:current-page="page"
|
||||
:total="total"
|
||||
@current-change="onPageChange"
|
||||
/>
|
||||
<div class="unified-section table-section">
|
||||
<el-table :data="logs" v-loading="loading" style="width: 100%" stripe>
|
||||
<el-table-column prop="timestamp" :label="TEXT.modules.adminAuditLogs.columns.time" width="180" align="center">
|
||||
<template #default="scope">
|
||||
<span class="text-gray-500">{{ displayDateTime(scope.row.timestamp) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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" width="160">
|
||||
<template #default="scope">
|
||||
<el-tag effect="plain">{{ scope.row.eventLabel }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<strong>{{ scope.row.targetTypeLabel }}</strong>
|
||||
<span class="text-gray-500" v-if="scope.row.targetName">({{ scope.row.targetName }})</span>
|
||||
</span>
|
||||
<span v-else>{{ TEXT.common.fallback }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.diff" min-width="260">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.diffText?.length" class="diff-container">
|
||||
<div v-for="(line, idx) in scope.row.diffText" :key="idx" class="diff-line">{{ line }}</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">{{ TEXT.audit.emptyValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.adminAuditLogs.columns.result" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.result === 'SUCCESS' ? 'success' : 'danger'" effect="dark" size="small">
|
||||
{{ scope.row.resultLabel }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
layout="prev, pager, next"
|
||||
:page-size="pageSize"
|
||||
:current-page="page"
|
||||
:total="total"
|
||||
@current-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -141,6 +148,7 @@ const pageSize = 20;
|
||||
const total = ref(0);
|
||||
|
||||
const filters = ref({
|
||||
entityType: "",
|
||||
eventType: "",
|
||||
operatorId: "",
|
||||
result: "",
|
||||
@@ -151,6 +159,15 @@ const eventTypeOptions = Object.entries(auditDict).map(([value, cfg]) => ({
|
||||
value,
|
||||
label: cfg.label,
|
||||
}));
|
||||
const entityTypeOptions = [
|
||||
{ value: "study_setup_config", label: "立项配置" },
|
||||
{ value: "study", label: "项目" },
|
||||
{ value: "subject", label: "参与者" },
|
||||
{ value: "site", label: "中心" },
|
||||
{ value: "finance", label: "费用" },
|
||||
{ value: "drug_shipment", label: "药品流向" },
|
||||
{ value: "audit_log", label: "审计日志" },
|
||||
];
|
||||
const userOptions = computed(() => users.value.map((u: any) => ({ label: u.username, value: u.id })));
|
||||
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
||||
const canProjectExport = computed(() => !!study.currentStudy && auth.user?.role === "ADMIN");
|
||||
@@ -188,6 +205,7 @@ const loadLogs = async () => {
|
||||
const params: Record<string, any> = {
|
||||
skip: (page.value - 1) * pageSize,
|
||||
limit: pageSize,
|
||||
entity_type: filters.value.entityType || undefined,
|
||||
action: filters.value.eventType || undefined,
|
||||
operator_id: filters.value.operatorId || undefined,
|
||||
};
|
||||
@@ -258,6 +276,7 @@ const fetchAllForExport = async () => {
|
||||
const params: Record<string, any> = {
|
||||
skip: 0,
|
||||
limit: 2000,
|
||||
entity_type: filters.value.entityType || undefined,
|
||||
action: filters.value.eventType || undefined,
|
||||
operator_id: filters.value.operatorId || undefined,
|
||||
};
|
||||
@@ -327,12 +346,9 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
.table-section {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,19 +20,6 @@
|
||||
<el-option :label="TEXT.enums.projectStatus.CLOSED" value="CLOSED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">{{ TEXT.modules.adminProjects.visitTemplate }}</el-divider>
|
||||
<el-form-item :label="TEXT.common.fields.visitTotal">
|
||||
<el-input-number v-model="form.visit_total" :min="1" :max="50" :step="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.visitIntervalDays">
|
||||
<el-input-number v-model="form.visit_interval_days" :min="1" :max="365" :step="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.visitWindowStartOffset">
|
||||
<el-input-number v-model="form.visit_window_start_offset" :min="-365" :max="365" :step="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.common.fields.visitWindowEndOffset">
|
||||
<el-input-number v-model="form.visit_window_end_offset" :min="-365" :max="365" :step="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
@@ -71,14 +58,11 @@ const form = reactive({
|
||||
protocol_no: "",
|
||||
phase: "",
|
||||
status: "DRAFT",
|
||||
visit_interval_days: null as number | null,
|
||||
visit_total: null as number | null,
|
||||
visit_window_start_offset: null as number | null,
|
||||
visit_window_end_offset: null as number | null,
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: requiredMessage(TEXT.common.fields.projectName), trigger: "blur" }],
|
||||
code: [{ required: true, message: requiredMessage(TEXT.common.fields.projectCode), trigger: "blur" }],
|
||||
status: [{ required: true, message: requiredMessage(TEXT.common.fields.status), trigger: "change" }],
|
||||
});
|
||||
|
||||
@@ -88,10 +72,6 @@ const resetForm = () => {
|
||||
form.protocol_no = "";
|
||||
form.phase = "";
|
||||
form.status = "DRAFT";
|
||||
form.visit_interval_days = null;
|
||||
form.visit_total = null;
|
||||
form.visit_window_start_offset = null;
|
||||
form.visit_window_end_offset = null;
|
||||
};
|
||||
|
||||
watch(
|
||||
@@ -105,10 +85,6 @@ watch(
|
||||
form.protocol_no = props.project.protocol_no || "";
|
||||
form.phase = props.project.phase || "";
|
||||
form.status = props.project.status || "DRAFT";
|
||||
form.visit_interval_days = props.project.visit_interval_days ?? null;
|
||||
form.visit_total = props.project.visit_total ?? null;
|
||||
form.visit_window_start_offset = props.project.visit_window_start_offset ?? null;
|
||||
form.visit_window_end_offset = props.project.visit_window_end_offset ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,27 +98,19 @@ const onSubmit = async () => {
|
||||
if (props.project) {
|
||||
await updateStudy(props.project.id, {
|
||||
name: form.name,
|
||||
code: form.code,
|
||||
code: form.code.trim(),
|
||||
protocol_no: form.protocol_no,
|
||||
phase: form.phase,
|
||||
status: form.status,
|
||||
visit_interval_days: form.visit_interval_days,
|
||||
visit_total: form.visit_total,
|
||||
visit_window_start_offset: form.visit_window_start_offset,
|
||||
visit_window_end_offset: form.visit_window_end_offset,
|
||||
});
|
||||
ElMessage.success(TEXT.modules.adminProjects.updateSuccess);
|
||||
} else {
|
||||
await createStudy({
|
||||
name: form.name,
|
||||
code: form.code,
|
||||
code: form.code.trim(),
|
||||
protocol_no: form.protocol_no,
|
||||
phase: form.phase,
|
||||
status: form.status,
|
||||
visit_interval_days: form.visit_interval_days,
|
||||
visit_total: form.visit_total,
|
||||
visit_window_start_offset: form.visit_window_start_offset,
|
||||
visit_window_end_offset: form.visit_window_end_offset,
|
||||
});
|
||||
ElMessage.success(TEXT.modules.adminProjects.createSuccess);
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card shadow="never" class="main-content-card">
|
||||
<div class="filter-container">
|
||||
<div class="filter-form">
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openAdd" class="header-action-btn">
|
||||
{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjectMembers.memberLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-card shadow="never" class="main-content-card unified-shell">
|
||||
<div class="unified-action-bar actions-only-bar">
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openAdd">
|
||||
{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjectMembers.memberLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="unified-section member-table-section">
|
||||
<el-table :data="memberRows" v-loading="loading" stripe>
|
||||
<el-table-column prop="full_name" :label="TEXT.modules.adminProjectMembers.username" min-width="160" />
|
||||
<el-table-column prop="role_in_study" :label="TEXT.modules.adminProjectMembers.projectRole" min-width="140">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ displayEnum(TEXT.enums.userRole, scope.row.role_in_study) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.fields.status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tooltip
|
||||
v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
:content="TEXT.modules.adminProjectMembers.disabledGlobalHint"
|
||||
placement="top"
|
||||
>
|
||||
<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 ? TEXT.common.actions.enable : TEXT.modules.adminProjectMembers.disabled }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="added_at" :label="TEXT.modules.adminProjectMembers.addedAt" min-width="180">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.added_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="300" align="center" class-name="action-column">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.role_in_study"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
@change="(val: string) => updateRole(scope.row.id, val)"
|
||||
:disabled="!scope.row.is_active || scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
>
|
||||
<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">{{ 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'"
|
||||
size="small"
|
||||
:disabled="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
@click="toggleActive(scope.row)"
|
||||
>
|
||||
{{ scope.row.is_active ? TEXT.common.actions.disable : TEXT.common.actions.enable }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-table :data="memberRows" v-loading="loading" stripe>
|
||||
<el-table-column prop="full_name" :label="TEXT.modules.adminProjectMembers.username" min-width="160" />
|
||||
<el-table-column prop="role_in_study" :label="TEXT.modules.adminProjectMembers.projectRole" min-width="140">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ displayEnum(TEXT.enums.userRole, scope.row.role_in_study) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.fields.status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tooltip
|
||||
v-if="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
:content="TEXT.modules.adminProjectMembers.disabledGlobalHint"
|
||||
placement="top"
|
||||
>
|
||||
<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 ? TEXT.common.actions.enable : TEXT.modules.adminProjectMembers.disabled }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="added_at" :label="TEXT.modules.adminProjectMembers.addedAt" min-width="180">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.added_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" width="300" align="center" class-name="action-column">
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.role_in_study"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
@change="(val: string) => updateRole(scope.row.id, val)"
|
||||
:disabled="!scope.row.is_active || scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
>
|
||||
<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">{{ 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'"
|
||||
size="small"
|
||||
:disabled="scope.row.effectiveStatus === 'DISABLED_GLOBAL'"
|
||||
@click="toggleActive(scope.row)"
|
||||
>
|
||||
{{ 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="TEXT.modules.adminProjectMembers.newTitle" width="520px" v-model="addVisible" :close-on-click-modal="false">
|
||||
@@ -399,29 +399,15 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
.actions-only-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
border-radius: 8px;
|
||||
.member-table-section {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card shadow="never" class="main-content-card">
|
||||
<div class="filter-container">
|
||||
<div class="filter-form">
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openCreate" class="header-action-btn">
|
||||
{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjects.projectLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-card shadow="never" class="main-content-card unified-shell">
|
||||
<div class="unified-action-bar actions-only-bar">
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openCreate">
|
||||
{{ TEXT.common.actions.add }}{{ TEXT.modules.adminProjects.projectLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="unified-section">
|
||||
<el-table :data="projects" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.projectName" min-width="300">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="goDetail(scope.row)" class="font-medium">{{ scope.row.name }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="code" :label="TEXT.common.fields.projectCode" min-width="160">
|
||||
<template #default="scope">
|
||||
<span class="text-gray-500">{{ scope.row.code || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" min-width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTag(scope.row.status)" effect="plain" round>{{ statusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_locked" label="锁定状态" min-width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.is_locked" type="warning" effect="plain" round>已锁定</el-tag>
|
||||
<el-tag v-else type="success" effect="plain" round>未锁定</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" min-width="280" align="center">
|
||||
<template #default="scope">
|
||||
<el-tooltip :content="TEXT.modules.adminProjects.members" placement="top">
|
||||
<el-button link type="primary" :icon="User" class="action-btn" @click="goMembers(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.modules.adminProjects.sites" placement="top">
|
||||
<el-button link type="primary" :icon="OfficeBuilding" class="action-btn" @click="goSites(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.modules.adminProjects.enter" placement="top">
|
||||
<el-button link type="success" :icon="ArrowRight" class="action-btn enter-btn" @click="enterStudy(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="!scope.row.is_locked" content="锁定项目" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
:icon="Lock"
|
||||
class="action-btn"
|
||||
@click="handleLockToggle(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.common.actions.delete" placement="top">
|
||||
<el-button link type="danger" :icon="Delete" class="action-btn" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-table :data="projects" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.projectName" min-width="300">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="goDetail(scope.row)" class="font-medium">{{ scope.row.name }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="code" :label="TEXT.common.fields.projectCode" min-width="160">
|
||||
<template #default="scope">
|
||||
<span class="text-gray-500">{{ scope.row.code || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" :label="TEXT.common.fields.status" min-width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusTag(scope.row.status)" effect="plain" round>{{ statusLabel(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_locked" label="锁定状态" min-width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.is_locked" type="warning" effect="plain" round>已锁定</el-tag>
|
||||
<el-tag v-else type="success" effect="plain" round>未锁定</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" min-width="280" align="center">
|
||||
<template #default="scope">
|
||||
<el-tooltip :content="TEXT.modules.adminProjects.members" placement="top">
|
||||
<el-button link type="primary" :icon="User" class="action-btn" @click="goMembers(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.modules.adminProjects.sites" placement="top">
|
||||
<el-button link type="primary" :icon="OfficeBuilding" class="action-btn" @click="goSites(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.modules.adminProjects.enter" placement="top">
|
||||
<el-button link type="success" :icon="ArrowRight" class="action-btn enter-btn" @click="enterStudy(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="!scope.row.is_locked" content="锁定项目" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
:icon="Lock"
|
||||
class="action-btn"
|
||||
@click="handleLockToggle(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.common.actions.delete" placement="top">
|
||||
<el-button link type="danger" :icon="Delete" class="action-btn" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<ProjectForm v-model:visible="formVisible" :project="editingProject" @saved="loadProjects" />
|
||||
</div>
|
||||
@@ -197,7 +197,7 @@ const enterStudy = (row: Study) => {
|
||||
};
|
||||
|
||||
const goDetail = (row: Study) => {
|
||||
router.push(`/projects/${row.id}`);
|
||||
router.push(`/admin/projects/${row.id}`);
|
||||
};
|
||||
|
||||
const statusLabel = (status: string) =>
|
||||
@@ -221,52 +221,19 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
.actions-only-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-spacer {
|
||||
flex: 1;
|
||||
.unified-section {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 表格样式美化 */
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: #f9fafb;
|
||||
--el-table-header-text-color: #374151;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
background-color: #f9fafb;
|
||||
font-weight: 600;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-table__cell) {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* 操作按钮美化 */
|
||||
.action-btn {
|
||||
font-size: 18px; /* 图标变更大 */
|
||||
font-size: 18px;
|
||||
padding: 4px;
|
||||
margin: 0 2px;
|
||||
transition: all 0.3s;
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="入组目标" prop="enrollment_target">
|
||||
<el-input-number v-model="form.enrollment_target" :min="0" :step="1" :placeholder="'请输入入组目标人数'" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
@@ -77,7 +74,6 @@ const form = reactive({
|
||||
phone: "",
|
||||
contact: "",
|
||||
craSelections: [] as string[],
|
||||
enrollment_target: 0,
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
@@ -103,7 +99,6 @@ const resetForm = () => {
|
||||
form.phone = "";
|
||||
form.contact = "";
|
||||
form.craSelections = [];
|
||||
form.enrollment_target = 0;
|
||||
form.is_active = true;
|
||||
};
|
||||
|
||||
@@ -127,7 +122,6 @@ watch(
|
||||
optionByLabel[opt.label] = opt.value;
|
||||
});
|
||||
form.craSelections = rawSelections.map((s) => optionByLabel[s] || s);
|
||||
form.enrollment_target = (props.site as any)?.enrollment_target || 0;
|
||||
form.is_active = props.site.is_active;
|
||||
}
|
||||
}
|
||||
@@ -161,7 +155,6 @@ const onSubmit = async () => {
|
||||
city: form.city,
|
||||
pi_name: form.pi_name,
|
||||
contact,
|
||||
enrollment_target: form.enrollment_target,
|
||||
is_active: form.is_active,
|
||||
});
|
||||
ElMessage.success(TEXT.modules.adminSites.updateSuccess);
|
||||
@@ -177,7 +170,6 @@ const onSubmit = async () => {
|
||||
city: form.city,
|
||||
pi_name: form.pi_name,
|
||||
contact,
|
||||
enrollment_target: form.enrollment_target,
|
||||
});
|
||||
ElMessage.success(TEXT.modules.adminSites.createSuccess);
|
||||
logAudit("SITE_STATUS_CHANGED", {
|
||||
@@ -187,6 +179,7 @@ const onSubmit = async () => {
|
||||
severity: "normal",
|
||||
});
|
||||
}
|
||||
|
||||
emit("saved");
|
||||
visibleProxy.value = false;
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -1,57 +1,66 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card shadow="never" class="main-content-card">
|
||||
<div class="filter-container">
|
||||
<div class="filter-form">
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openCreate" class="header-action-btn">
|
||||
{{ TEXT.common.actions.add }}{{ TEXT.modules.adminSites.siteLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-card shadow="never" class="main-content-card unified-shell">
|
||||
<div class="unified-action-bar actions-only-bar">
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openCreate">
|
||||
{{ TEXT.common.actions.add }}{{ TEXT.modules.adminSites.siteLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="unified-section site-table-section">
|
||||
<el-table :data="displaySites" v-loading="loading" stripe :row-class-name="siteRowClass">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.siteName" width="220">
|
||||
<template #default="scope">
|
||||
<div class="site-name-cell">
|
||||
<span>{{ scope.row.name }}</span>
|
||||
<el-tag v-if="isLeadUnitSite(scope.row)" type="warning" effect="light" size="small">
|
||||
{{ TEXT.modules.adminSites.leadUnitTag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="city" :label="TEXT.common.fields.city" width="100" />
|
||||
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" width="120" />
|
||||
<el-table-column :label="TEXT.common.fields.phone" width="140">
|
||||
<template #default="scope">
|
||||
{{ phoneText(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contact" :label="TEXT.common.fields.contact" width="160">
|
||||
<template #default="scope">
|
||||
{{ contactLabel(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.fields.status" width="100">
|
||||
<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="TEXT.common.labels.actions" width="200" align="center" class-name="action-column">
|
||||
<template #default="scope">
|
||||
<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 ? "锁定" : "解锁" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isAdmin"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="confirmDelete(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-table :data="sites" v-loading="loading" stripe :row-class-name="siteRowClass">
|
||||
<el-table-column prop="name" :label="TEXT.common.fields.siteName" width="180" />
|
||||
<el-table-column prop="city" :label="TEXT.common.fields.city" width="100" />
|
||||
<el-table-column prop="pi_name" :label="TEXT.modules.adminSites.piLabel" width="120" />
|
||||
<el-table-column :label="TEXT.common.fields.phone" width="140">
|
||||
<template #default="scope">
|
||||
{{ phoneText(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="contact" :label="TEXT.common.fields.contact" width="160">
|
||||
<template #default="scope">
|
||||
{{ contactLabel(scope.row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.fields.status" width="100">
|
||||
<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="TEXT.common.labels.actions" width="200" align="center" class-name="action-column">
|
||||
<template #default="scope">
|
||||
<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 ? "锁定" : "解锁" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isAdmin"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="confirmDelete(scope.row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<SiteForm
|
||||
v-if="projectId"
|
||||
@@ -158,7 +167,32 @@ const contactLabel = (row: any) => {
|
||||
|
||||
const phoneText = (row: any) => row?.phone || row?.contact_phone || TEXT.common.fallback;
|
||||
|
||||
const siteRowClass = ({ row }: { row: Site }) => (row.is_active ? "" : "row-inactive");
|
||||
const normalizeSiteName = (value?: string | null) => String(value || "").trim().replace(/\s+/g, "").toLowerCase();
|
||||
const isLeadUnitSite = (row: Site): boolean => {
|
||||
const leadUnitName = normalizeSiteName(project.value?.lead_unit);
|
||||
const siteName = normalizeSiteName(row.name);
|
||||
if (!leadUnitName || !siteName) return false;
|
||||
return siteName === leadUnitName;
|
||||
};
|
||||
|
||||
const displaySites = computed(() => {
|
||||
return sites.value
|
||||
.map((item, index) => ({ item, index }))
|
||||
.sort((a, b) => {
|
||||
const aLead = isLeadUnitSite(a.item) ? 1 : 0;
|
||||
const bLead = isLeadUnitSite(b.item) ? 1 : 0;
|
||||
if (aLead !== bLead) return bLead - aLead;
|
||||
return a.index - b.index;
|
||||
})
|
||||
.map((entry) => entry.item);
|
||||
});
|
||||
|
||||
const siteRowClass = ({ row }: { row: Site }) => {
|
||||
const classes: string[] = [];
|
||||
if (!row.is_active) classes.push("row-inactive");
|
||||
if (isLeadUnitSite(row)) classes.push("row-lead-unit");
|
||||
return classes.join(" ");
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
loadUsers();
|
||||
@@ -271,29 +305,21 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
.actions-only-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-spacer {
|
||||
flex: 1;
|
||||
.site-table-section {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
border-radius: 8px;
|
||||
.site-name-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -308,6 +334,14 @@ onMounted(async () => {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.row-lead-unit td {
|
||||
background-color: #f3f8ff !important;
|
||||
}
|
||||
|
||||
.row-lead-unit.row-inactive td {
|
||||
background-color: #edf2fb !important;
|
||||
}
|
||||
|
||||
.el-table .action-column .cell {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<el-card shadow="never" class="main-content-card">
|
||||
<div class="filter-container">
|
||||
<el-card shadow="never" class="main-content-card unified-shell">
|
||||
<div class="filter-container unified-action-bar">
|
||||
<div class="filter-form">
|
||||
<el-form-item label="" class="filter-item-form">
|
||||
<el-input
|
||||
@@ -23,62 +23,64 @@
|
||||
<el-button @click="loadUsers">{{ TEXT.common.actions.search }}</el-button>
|
||||
</el-form-item>
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openCreate" class="header-action-btn">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminUsers.userLabel }}</el-button>
|
||||
<el-button type="primary" @click="openCreate">{{ TEXT.common.actions.add }}{{ TEXT.modules.adminUsers.userLabel }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="users" stripe v-loading="loading" style="width: 100%">
|
||||
<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="TEXT.modules.adminUsers.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" min-width="200" align="center">
|
||||
<template #default="scope">
|
||||
<el-tooltip :content="TEXT.common.actions.edit" placement="top">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEdit(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="scope.row.status === 'ACTIVE' ? TEXT.common.actions.disable : TEXT.common.actions.enable" placement="top">
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="scope.row.status === 'ACTIVE' ? Lock : Unlock"
|
||||
:disabled="isLastAdmin(scope.row)"
|
||||
@click="toggleStatus(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.modules.adminUsers.resetPassword" placement="top">
|
||||
<el-button link type="warning" :icon="Key" @click="openReset(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.common.actions.delete" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
:disabled="scope.row.id === auth.user?.id"
|
||||
@click="onDelete(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next, total"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
v-model:current-page="page"
|
||||
@current-change="loadUsers"
|
||||
class="pagination"
|
||||
/>
|
||||
<div class="unified-section user-table-section">
|
||||
<el-table :data="users" stripe v-loading="loading" style="width: 100%">
|
||||
<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="TEXT.modules.adminUsers.createdAt" width="200">
|
||||
<template #default="scope">{{ displayDateTime(scope.row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.common.labels.actions" min-width="200" align="center">
|
||||
<template #default="scope">
|
||||
<el-tooltip :content="TEXT.common.actions.edit" placement="top">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEdit(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="scope.row.status === 'ACTIVE' ? TEXT.common.actions.disable : TEXT.common.actions.enable" placement="top">
|
||||
<el-button
|
||||
link
|
||||
:type="scope.row.status === 'ACTIVE' ? 'warning' : 'success'"
|
||||
:icon="scope.row.status === 'ACTIVE' ? Lock : Unlock"
|
||||
:disabled="isLastAdmin(scope.row)"
|
||||
@click="toggleStatus(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.modules.adminUsers.resetPassword" placement="top">
|
||||
<el-button link type="warning" :icon="Key" @click="openReset(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="TEXT.common.actions.delete" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
:disabled="scope.row.id === auth.user?.id"
|
||||
@click="onDelete(scope.row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next, total"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
v-model:current-page="page"
|
||||
@current-change="loadUsers"
|
||||
class="pagination"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<UserForm v-model:visible="formVisible" :user="editingUser" :admin-count="activeAdminCount" @saved="loadUsers" />
|
||||
<UserResetPassword v-model:visible="resetVisible" :user="resetUser" @reset="loadUsers" />
|
||||
@@ -282,14 +284,6 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -310,10 +304,9 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
border-radius: 8px;
|
||||
.user-table-section {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
|
||||
Reference in New Issue
Block a user