立项配置页初步优化

This commit is contained in:
Cheng Zhou
2026-02-24 16:53:22 +08:00
parent 0693f09b1d
commit 8f3f717e48
124 changed files with 12519 additions and 2954 deletions
+25 -21
View File
@@ -9,8 +9,8 @@
/>
</el-col>
<el-col :span="19">
<el-card class="mb-12">
<div class="filters">
<div class="faq-main unified-shell">
<div class="filters unified-action-bar">
<el-input
v-model="keyword"
:placeholder="TEXT.common.placeholders.keyword"
@@ -24,23 +24,23 @@
<el-button type="primary" @click="openForm()">{{ TEXT.modules.knowledgeMedicalConsult.newItem }}</el-button>
</PermissionAction>
</div>
</el-card>
<FaqList
:items="faqs"
:categories="categories"
:loading="loading"
:can-edit="canEdit"
@refresh="loadFaqs"
/>
<el-pagination
class="pagination"
layout="prev, pager, next"
:page-size="pageSize"
:current-page="page"
:total="total"
@current-change="onPageChange"
/>
<FaqList
:items="faqs"
:categories="categories"
:loading="loading"
:can-edit="canEdit"
@refresh="loadFaqs"
/>
<el-pagination
class="pagination"
layout="prev, pager, next"
:page-size="pageSize"
:current-page="page"
:total="total"
@current-change="onPageChange"
/>
</div>
</el-col>
</el-row>
@@ -146,10 +146,17 @@ onMounted(async () => {
.page {
padding: 16px;
}
.faq-main {
background: #fff;
border: 1px solid var(--ctms-border-color);
border-radius: 10px;
padding: 16px;
}
.filters {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 16px;
}
.spacer {
flex: 1;
@@ -158,7 +165,4 @@ onMounted(async () => {
margin-top: 12px;
text-align: right;
}
.mb-12 {
margin-bottom: 12px;
}
</style>
+3 -3
View File
@@ -1,6 +1,6 @@
<template>
<div class="page">
<el-card v-loading="loading">
<div class="page unified-shell">
<el-card class="unified-shell" v-loading="loading">
<div class="content">
<div class="question-header">
<div class="question-label">{{ TEXT.modules.knowledgeMedicalConsult.questionDesc }}</div>
@@ -36,7 +36,7 @@
</div>
</el-card>
<el-card class="mt-12" v-loading="repliesLoading">
<el-card class="mt-12 unified-shell" v-loading="repliesLoading">
<template #header>
<div class="reply-header">
<span>{{ TEXT.modules.knowledgeMedicalConsult.replies }}</span>
+11 -1
View File
@@ -118,6 +118,7 @@ import { useAuthStore } from "../store/auth";
import { useStudyStore } from "../store/study";
import { getCachedCredential, setCachedCredential, clearCachedCredential } from "../utils/auth";
import { TEXT, requiredMessage } from "../locales";
import { consumeLogoutReason, LOGOUT_REASON_TIMEOUT } from "../session/sessionManager";
const auth = useAuthStore();
const router = useRouter();
@@ -143,6 +144,10 @@ let vantaEffect: any = null;
// 初始化 Vanta.js 效果
onMounted(() => {
const logoutReason = consumeLogoutReason();
if (logoutReason === LOGOUT_REASON_TIMEOUT) {
ElMessage.warning("长时间未操作,已自动退出登录,请重新登录");
}
const cached = getCachedCredential();
if (cached) {
form.email = cached.email;
@@ -228,8 +233,13 @@ const onSubmit = async () => {
router.push("/");
}
} catch (error: any) {
const status = error?.response?.status;
const detail = error?.response?.data?.detail || error?.response?.data?.message;
ElMessage.error(detail || TEXT.modules.auth.authFailed);
if (!status || status >= 500) {
ElMessage.error("服务暂时不可用,请稍后重试");
} else {
ElMessage.error(detail || TEXT.modules.auth.authFailed);
}
} finally {
loading.value = false;
}
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div class="page">
<el-card>
<el-card class="unified-shell">
<h3 class="title">{{ TEXT.modules.profile.title }}</h3>
<p class="subtitle">{{ TEXT.modules.profile.subtitle }}</p>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form">
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<div class="study-home-container" v-if="study.currentStudy">
<div class="study-home-container unified-shell" v-if="study.currentStudy">
<!-- 概览头部 -->
<div class="home-header">
<div class="header-content">
@@ -68,7 +68,7 @@
/>
<!-- 通知 -->
<el-card class="notifications-card">
<el-card class="notifications-card unified-shell">
<div class="notifications-header">
<span class="notifications-title">{{ TEXT.modules.projectOverview.notificationsTitle }}</span>
<span class="notifications-subtitle">{{ TEXT.modules.projectOverview.notificationsSubtitle }}</span>
+48 -49
View File
@@ -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>
+71 -55
View File
@@ -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
+3 -35
View File
@@ -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);
}
+66 -80
View File
@@ -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 {
+63 -96
View File
@@ -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;
+1 -8
View File
@@ -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) {
+103 -69
View File
@@ -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;
+60 -67
View File
@@ -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 {
+57 -41
View File
@@ -1,43 +1,44 @@
<template>
<div class="ctms-page">
<div class="ctms-page-header">
<div>
<h1 class="ctms-page-title">
{{ detail.title || TEXT.modules.fileVersionManagement.title }}
</h1>
<p class="ctms-page-subtitle">
<el-tag size="small" effect="plain" type="info" class="mr-2">{{ displayText(detail.doc_type, TEXT.enums.documentType) }}</el-tag>
<el-tag size="small" effect="plain" type="warning" class="mr-2">{{ displaySite(detail.site_id) }}</el-tag>
<el-tag v-if="isReadOnly" size="small" effect="plain" type="info">中心已停用</el-tag>
<span class="text-secondary text-sm">
{{ TEXT.modules.fileVersionManagement.labels.currentEffective }}
<span class="font-medium text-main">{{ detail.current_effective_version?.version_no || TEXT.common.fallback }}</span>
</span>
</p>
</div>
<div class="ctms-page-actions">
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
<div class="page">
<div class="unified-shell">
<section class="unified-section" v-loading="loading">
<div class="unified-section-header">
<div>
<div class="ctms-section-title">
{{ detail.title || TEXT.modules.fileVersionManagement.title }}
</div>
<div class="doc-meta-row mt-1">
<el-tag size="small" effect="plain" type="info" class="mr-2">{{ displayText(detail.doc_type, TEXT.enums.documentType) }}</el-tag>
<el-tag size="small" effect="plain" type="warning" class="mr-2">{{ displaySite(detail.site_id) }}</el-tag>
<el-tag v-if="isReadOnly" size="small" effect="plain" type="info">中心已停用</el-tag>
<span class="text-secondary text-sm">
{{ TEXT.modules.fileVersionManagement.labels.currentEffective }}
<span class="font-medium text-main">{{ detail.current_effective_version?.version_no || TEXT.common.fallback }}</span>
</span>
</div>
</div>
<div class="ctms-section-actions">
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
<el-descriptions :column="3" border class="ctms-descriptions">
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.fields.docType">{{ displayText(detail.doc_type, TEXT.enums.documentType) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.fields.site">{{ displaySite(detail.site_id) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.labels.currentEffective">
{{ currentEffectiveVersion?.version_no || TEXT.common.fallback }}
</el-descriptions-item>
<el-descriptions-item label="版本日期">
{{ formatDateOnly(currentEffectiveVersion?.effective_at || currentEffectiveVersion?.created_at || null) }}
</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.columns.updatedAt">{{ formatDate(detail.updated_at) }}</el-descriptions-item>
</el-descriptions>
</section>
<el-card class="ctms-section-card" v-loading="loading">
<el-descriptions :column="3" border class="ctms-descriptions">
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.fields.docType">{{ displayText(detail.doc_type, TEXT.enums.documentType) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.fields.site">{{ displaySite(detail.site_id) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.labels.currentEffective">
{{ currentEffectiveVersion?.version_no || TEXT.common.fallback }}
</el-descriptions-item>
<el-descriptions-item label="版本日期">
{{ formatDateOnly(currentEffectiveVersion?.effective_at || currentEffectiveVersion?.created_at || null) }}
</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.columns.updatedAt">{{ formatDate(detail.updated_at) }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="ctms-tabs">
<section class="unified-section">
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="ctms-tabs">
<el-tab-pane :label="TEXT.modules.fileVersionManagement.tabs.versions" name="versions">
<div class="ctms-section-header">
<div class="ctms-section-title">&nbsp;</div>
@@ -48,7 +49,7 @@
</el-button>
</div>
</div>
<el-card class="ctms-table-card">
<el-card class="ctms-table-card unified-shell">
<el-table :data="detail.version_timeline" v-loading="versionLoading" class="ctms-table">
<el-table-column prop="version_no" :label="TEXT.modules.fileVersionManagement.fields.versionNo" width="120">
<template #default="{ row }">
@@ -106,7 +107,7 @@
</el-button>
</div>
</div>
<el-card class="ctms-table-card">
<el-card class="ctms-table-card unified-shell">
<el-table :data="distributions" v-loading="distributionLoading" class="ctms-table">
<el-table-column :label="TEXT.modules.fileVersionManagement.fields.targetType" width="120">
<template #default="{ row }">
@@ -127,7 +128,9 @@
</el-card>
</el-tab-pane>
</el-tabs>
</el-tabs>
</section>
</div>
<el-dialog v-model="uploadVisible" :title="TEXT.modules.fileVersionManagement.dialog.uploadTitle" width="520px" destroy-on-close class="ctms-dialog">
<el-form ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="110px" class="ctms-form">
@@ -671,6 +674,19 @@ watch(
</script>
<style scoped>
.page {
display: flex;
flex-direction: column;
gap: 0;
}
.doc-meta-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.text-secondary {
color: var(--ctms-text-secondary);
}
+66 -64
View File
@@ -1,8 +1,7 @@
<template>
<div class="ctms-page">
<el-card shadow="never" class="main-content-card">
<div class="filter-container">
<div class="page">
<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.scope_type" :placeholder="TEXT.modules.fileVersionManagement.filters.scope" clearable class="filter-select-comp">
@@ -29,60 +28,62 @@
</el-button>
</el-form>
</div>
<el-table
:data="sortedItems"
v-loading="loading"
@row-click="handleRowClick"
:row-class-name="documentRowClass"
class="ctms-table"
>
<el-table-column prop="title" :label="TEXT.modules.fileVersionManagement.columns.title" min-width="200">
<template #default="{ row }">
<span class="font-semibold">{{ row.title }}</span>
<section class="unified-section">
<el-table
:data="sortedItems"
v-loading="loading"
@row-click="handleRowClick"
:row-class-name="documentRowClass"
class="ctms-table"
>
<el-table-column prop="title" :label="TEXT.modules.fileVersionManagement.columns.title" min-width="200">
<template #default="{ row }">
<span class="font-semibold">{{ row.title }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.scope" width="120">
<template #default="{ row }">
<el-tag
effect="plain"
:type="row.scope_type === 'SITE' ? 'warning' : 'success'"
>
{{ displayEnum(TEXT.enums.scopeType, row.scope_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.site" min-width="160">
<template #default="{ row }">
<span>{{ displaySite(row.site_id) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.docType" width="140">
<template #default="{ row }">
<el-tag effect="plain" type="info">{{ displayText(row.doc_type, TEXT.enums.documentType) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.currentVersion" width="140">
<template #default="{ row }">
<span v-if="row.current_effective_version">{{ row.current_effective_version.version_no }}</span>
<span v-else class="text-secondary">-</span>
</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.modules.fileVersionManagement.columns.updatedAt" width="180">
<template #default="{ row }">
<span class="text-secondary">{{ formatDate(row.updated_at) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="100" align="center">
<template #default="{ row }">
<el-button v-if="isAdmin" link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty :description="TEXT.modules.fileVersionManagement.emptyDescription" />
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.scope" width="120">
<template #default="{ row }">
<el-tag
effect="plain"
:type="row.scope_type === 'SITE' ? 'warning' : 'success'"
>
{{ displayEnum(TEXT.enums.scopeType, row.scope_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.site" min-width="160">
<template #default="{ row }">
<span>{{ displaySite(row.site_id) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.docType" width="140">
<template #default="{ row }">
<el-tag effect="plain" type="info">{{ displayText(row.doc_type, TEXT.enums.documentType) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.currentVersion" width="140">
<template #default="{ row }">
<span v-if="row.current_effective_version">{{ row.current_effective_version.version_no }}</span>
<span v-else class="text-secondary">-</span>
</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.modules.fileVersionManagement.columns.updatedAt" width="180">
<template #default="{ row }">
<span class="text-secondary">{{ formatDate(row.updated_at) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="100" align="center">
<template #default="{ row }">
<el-button v-if="isAdmin" link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty :description="TEXT.modules.fileVersionManagement.emptyDescription" />
</template>
</el-table>
</el-table>
</section>
</el-card>
<el-dialog v-model="createVisible" :title="TEXT.modules.fileVersionManagement.dialog.createTitle" width="520px" destroy-on-close class="ctms-dialog">
@@ -339,17 +340,18 @@ watch(
</script>
<style scoped>
.ctms-page {
.page {
display: flex;
flex-direction: column;
gap: 0;
}
.main-content-card :deep(.el-card__body) {
padding: 20px;
padding: 0;
}
.filter-container {
margin-bottom: 20px;
margin-bottom: 0;
}
.filter-form {
@@ -365,7 +367,7 @@ watch(
}
.filter-select-comp {
width: 140px;
width: 132px;
}
.filter-spacer {
@@ -373,9 +375,9 @@ watch(
}
.header-action-btn {
height: 36px;
padding: 0 20px;
border-radius: 8px;
height: 32px;
padding: 0 12px;
border-radius: 10px;
}
</style>
+31 -60
View File
@@ -1,26 +1,20 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ TEXT.modules.drugShipments.detailTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.drugShipments.detailSubtitle }}</p>
</div>
<div class="actions">
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<div class="content">
<el-card>
<template #header>
<div class="card-header">
<span>{{ TEXT.common.labels.basicInfo }}</span>
<el-tag :type="detail.status === 'PENDING' ? 'info' : detail.status === 'SIGNED' ? 'success' : 'primary'">
{{ displayEnum(TEXT.enums.shipmentStatus, detail.status) }}
</el-tag>
<div class="unified-shell">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ TEXT.modules.drugShipments.detailTitle }}</div>
<div class="ctms-section-actions">
<el-button type="primary" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</template>
</div>
<div class="section-title-row">
<span class="ctms-section-title">{{ TEXT.common.labels.basicInfo }}</span>
<el-tag :type="detail.status === 'PENDING' ? 'info' : detail.status === 'SIGNED' ? 'success' : 'primary'">
{{ displayEnum(TEXT.enums.shipmentStatus, detail.status) }}
</el-tag>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.direction">
@@ -29,9 +23,9 @@
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card :header="TEXT.modules.drugShipments.recordLabel">
</section>
<section class="unified-section">
<div class="ctms-section-title section-title-inline">{{ TEXT.modules.drugShipments.recordLabel }}</div>
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.trackingNo">{{ detail.tracking_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.carrier">{{ detail.carrier || TEXT.common.fallback }}</el-descriptions-item>
@@ -41,15 +35,16 @@
<el-descriptions-item :label="TEXT.common.fields.batchNo">{{ detail.batch_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</el-card>
</section>
<section class="unified-section">
<AttachmentList
v-if="shipmentId"
:study-id="studyId"
entity-type="drug_shipment"
:entity-id="shipmentId"
/>
</section>
</div>
<AttachmentList
v-if="shipmentId"
:study-id="studyId"
entity-type="drug_shipment"
:entity-id="shipmentId"
/>
</div>
</template>
@@ -111,41 +106,17 @@ onMounted(load);
.page {
display: flex;
flex-direction: column;
gap: 16px;
gap: 0;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.content {
display: flex;
flex-direction: column;
gap: 16px;
}
.card-header {
.section-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.actions {
display: flex;
gap: 8px;
.section-title-inline {
margin-bottom: 12px;
}
</style>
+102 -125
View File
@@ -1,109 +1,108 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.drugShipments.editTitle : TEXT.modules.drugShipments.newTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.drugShipments.formSubtitle }}</p>
</div>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
<el-card>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="top">
<h3 class="section-title">{{ TEXT.common.labels.basicInfo }}</h3>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site" prop="center_id">
<el-select v-model="form.center_id" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly">
<el-option
v-for="site in sites"
:key="site.id"
:label="site.name || TEXT.common.fallback"
:value="site.id"
:disabled="!site.is_active"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.direction" prop="direction">
<el-select v-model="form.direction" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly">
<el-option :label="TEXT.enums.shipmentDirection.SEND" value="SEND" />
<el-option :label="TEXT.enums.shipmentDirection.RETURN" value="RETURN" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.status" prop="status">
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly">
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
<el-option :label="TEXT.enums.shipmentStatus.RETURNED" value="RETURNED" />
<el-option :label="TEXT.enums.shipmentStatus.EXCEPTION" value="EXCEPTION" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<h3 class="section-title">{{ TEXT.modules.drugShipments.recordLabel }}</h3>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.shipDate" prop="ship_date">
<el-date-picker v-model="form.ship_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.receiveDate" prop="receive_date">
<el-date-picker v-model="form.receive_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.quantity" prop="quantity">
<el-input-number v-model="form.quantity" :min="0" :controls="false" :placeholder="TEXT.common.placeholders.input" style="width: 100%" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.batchNo" prop="batch_no">
<el-input v-model="form.batch_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.batchNo" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.carrier" prop="carrier">
<el-input v-model="form.carrier" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.carrier" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.trackingNo" prop="tracking_no">
<el-input v-model="form.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" :disabled="isReadOnly" />
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="TEXT.common.fields.remark" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" :disabled="isReadOnly" />
</el-form-item>
<el-form-item>
<div class="actions">
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">
{{ TEXT.common.actions.save }}
</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
<div class="unified-shell">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ isEdit ? TEXT.modules.drugShipments.editTitle : TEXT.modules.drugShipments.newTitle }}</div>
<div class="ctms-section-actions">
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</el-form-item>
</el-form>
</el-card>
</div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="top">
<h3 class="section-title">{{ TEXT.common.labels.basicInfo }}</h3>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site" prop="center_id">
<el-select v-model="form.center_id" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly">
<el-option
v-for="site in sites"
:key="site.id"
:label="site.name || TEXT.common.fallback"
:value="site.id"
:disabled="!site.is_active"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.direction" prop="direction">
<el-select v-model="form.direction" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly">
<el-option :label="TEXT.enums.shipmentDirection.SEND" value="SEND" />
<el-option :label="TEXT.enums.shipmentDirection.RETURN" value="RETURN" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.status" prop="status">
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly">
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
<el-option :label="TEXT.enums.shipmentStatus.RETURNED" value="RETURNED" />
<el-option :label="TEXT.enums.shipmentStatus.EXCEPTION" value="EXCEPTION" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<AttachmentList
v-if="isEdit && shipmentId"
:study-id="studyId"
entity-type="drug_shipment"
:entity-id="shipmentId"
/>
<el-card v-else class="hint-card">
<StateEmpty :description="TEXT.modules.drugShipments.uploadHint" />
</el-card>
<h3 class="section-title">{{ TEXT.modules.drugShipments.recordLabel }}</h3>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.shipDate" prop="ship_date">
<el-date-picker v-model="form.ship_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.receiveDate" prop="receive_date">
<el-date-picker v-model="form.receive_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" style="width: 100%" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.quantity" prop="quantity">
<el-input-number v-model="form.quantity" :min="0" :controls="false" :placeholder="TEXT.common.placeholders.input" style="width: 100%" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.batchNo" prop="batch_no">
<el-input v-model="form.batch_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.batchNo" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.carrier" prop="carrier">
<el-input v-model="form.carrier" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.carrier" :disabled="isReadOnly" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.trackingNo" prop="tracking_no">
<el-input v-model="form.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" :disabled="isReadOnly" />
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="TEXT.common.fields.remark" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" :disabled="isReadOnly" />
</el-form-item>
<el-form-item>
<div class="actions">
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">
{{ TEXT.common.actions.save }}
</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</div>
</el-form-item>
</el-form>
</section>
<section class="unified-section">
<AttachmentList
v-if="isEdit && shipmentId"
:study-id="studyId"
entity-type="drug_shipment"
:entity-id="shipmentId"
/>
<StateEmpty v-else :description="TEXT.modules.drugShipments.uploadHint" />
</section>
</div>
</div>
</template>
@@ -245,29 +244,7 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.hint-card {
padding: 12px;
gap: 0;
}
.section-title {
+20 -34
View File
@@ -1,15 +1,5 @@
<template>
<div class="page">
<div class="actions">
<el-button type="primary" :disabled="!canWrite || isReadOnly" @click="goEdit" class="header-action-btn copy-btn">
<el-icon class="el-icon--left"><Edit /></el-icon>
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack" class="header-action-btn">
{{ TEXT.common.actions.back }}
</el-button>
</div>
<StateError v-if="errorMessage" :description="errorMessage">
<template #action>
<el-button type="primary" @click="load">{{ TEXT.common.actions.retry }}</el-button>
@@ -19,10 +9,19 @@
<StateLoading v-else-if="loading" :rows="6" />
<template v-else>
<el-card class="detail-card" shadow="never">
<el-card class="detail-card unified-shell" shadow="never">
<template #header>
<div class="card-header">
<div class="card-header actions-header">
<span class="header-title">{{ TEXT.common.labels.basicInfo }}</span>
<div class="actions">
<el-button type="primary" :disabled="!canWrite || isReadOnly" @click="goEdit" class="header-action-btn copy-btn">
<el-icon class="el-icon--left"><Edit /></el-icon>
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack" class="header-action-btn">
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
</template>
<el-descriptions :column="2" border class="custom-descriptions">
@@ -47,7 +46,7 @@
</el-descriptions>
</el-card>
<el-card class="section-card" shadow="never">
<el-card class="section-card unified-shell" shadow="never">
<template #header>
<div class="card-header">
<span class="header-title">{{ TEXT.modules.feeContracts.paymentTitle }}</span>
@@ -89,7 +88,7 @@
<StateEmpty v-if="detail.payments.length === 0" :description="TEXT.modules.feeContracts.paymentEmpty" />
</el-card>
<el-card class="section-card" shadow="never">
<el-card class="section-card unified-shell" shadow="never">
<template #header>
<div class="card-header">
<span class="header-title">{{ TEXT.modules.feeContracts.attachmentTitle }}</span>
@@ -232,26 +231,7 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 12px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: var(--ctms-text-main);
}
.page-subtitle {
margin: 6px 0 0;
font-size: 14px;
color: var(--ctms-text-secondary);
gap: 0;
}
.actions {
@@ -275,6 +255,12 @@ onMounted(async () => {
padding: 4px 0;
}
.actions-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
font-size: 16px;
font-weight: 600;
+7 -42
View File
@@ -1,15 +1,5 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.feeContracts.editTitle : TEXT.modules.feeContracts.newTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.feeContracts.formSubtitle }}</p>
</div>
<div class="actions">
<el-button @click="goBack" class="header-action-btn">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<StateError v-if="errorMessage" :description="errorMessage">
<template #action>
<el-button type="primary" @click="loadDetail">{{ TEXT.common.actions.retry }}</el-button>
@@ -19,11 +9,12 @@
<StateLoading v-else-if="loading" :rows="6" />
<template v-else>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px" label-position="top">
<el-card class="form-card" shadow="never">
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px" label-position="top">
<el-card class="form-card unified-shell" shadow="never">
<template #header>
<div class="card-header">
<div class="card-header actions-header">
<span class="header-title">{{ TEXT.common.labels.basicInfo }}</span>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</template>
<el-row :gutter="24">
@@ -97,7 +88,7 @@
</el-row>
</el-card>
<el-card class="form-card section-margin" shadow="never">
<el-card class="form-card section-margin unified-shell" shadow="never">
<template #header>
<div class="card-header actions-header">
<span class="header-title">{{ TEXT.modules.feeContracts.paymentTitle }}</span>
@@ -183,7 +174,7 @@
</div>
</el-card>
<el-card class="form-card section-margin" shadow="never">
<el-card class="form-card section-margin unified-shell" shadow="never">
<template #header>
<div class="card-header">
<span class="header-title">{{ TEXT.modules.feeContracts.attachmentTitle }}</span>
@@ -512,36 +503,10 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 24px;
max-width: 1000px;
margin: 0 auto;
gap: 0;
width: 100%;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: var(--ctms-text-main);
}
.page-subtitle {
margin: 6px 0 0;
font-size: 14px;
color: var(--ctms-text-secondary);
}
.header-action-btn {
height: 36px;
border-radius: 8px;
}
.form-card {
border-radius: 12px;
border: 1px solid var(--ctms-border-color);
+94 -93
View File
@@ -1,6 +1,5 @@
<template>
<div class="page">
<div class="overview">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
@@ -46,8 +45,16 @@
</el-row>
</div>
<el-card shadow="never" class="main-content-card" v-if="!errorMessage && !loading">
<div class="filter-container">
<StateError v-if="errorMessage" :description="errorMessage">
<template #action>
<el-button type="primary" @click="load">{{ TEXT.common.actions.retry }}</el-button>
</template>
</StateError>
<StateLoading v-else-if="loading" :rows="6" />
<el-card shadow="never" class="main-content-card unified-shell" v-else>
<div class="filter-container unified-action-bar">
<el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="" class="filter-item">
<el-select v-model="filters.centerId" clearable :placeholder="TEXT.common.fields.site" class="filter-select">
@@ -79,94 +86,87 @@
</el-button>
</el-form>
</div>
<StateError v-if="errorMessage" :description="errorMessage">
<template #action>
<el-button type="primary" @click="load">{{ TEXT.common.actions.retry }}</el-button>
</template>
</StateError>
<StateLoading v-else-if="loading" :rows="6" />
<el-table
:data="sortedContracts"
style="width: 100%"
@row-click="onRowClick"
:row-class-name="contractRowClass"
:header-cell-style="{ background: '#f8f9fb' }"
>
<el-table-column prop="center_name" :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">
<div class="site-cell">
<span class="site-name">{{ scope.row.center_name || TEXT.common.fallback }}</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.contractAmount" width="140" align="right">
<template #default="scope">
<span class="amount-text">{{ formatAmountWan(scope.row.contract_amount) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.caseProgress" min-width="140" align="center">
<template #default="scope">
<el-tag effect="plain" type="info">
{{ displayCases(scope.row.contract_cases, scope.row.actual_cases) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.paidSummary" min-width="200">
<template #default="scope">
<div class="summary-line">
<span>{{ TEXT.modules.feeContracts.paidTotal }}</span>
<span class="amount-highlight success">{{ formatAmountWan(scope.row.paid_total) }}</span>
</div>
<div class="summary-line">
<span>{{ TEXT.modules.feeContracts.unpaidBalance }}</span>
<span class="amount-highlight warning" v-if="scope.row.unpaid_balance > 0">{{ formatAmountWan(scope.row.unpaid_balance) }}</span>
<span class="amount-muted" v-else>0.00</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.verifySummary" min-width="200">
<template #default="scope">
<div class="summary-line">
<span>{{ TEXT.modules.feeContracts.verifiedTotal }}</span>
<span class="amount-highlight info">{{ formatAmountWan(scope.row.verified_total) }}</span>
</div>
<div class="summary-line">
<span>{{ TEXT.modules.feeContracts.unverifiedBalance }}</span>
<span class="amount-highlight warning" v-if="scope.row.unverified_balance > 0">{{ formatAmountWan(scope.row.unverified_balance) }}</span>
<span class="amount-muted" v-else>0.00</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.recentDates" min-width="180">
<template #default="scope">
<div class="date-line">
<span class="date-label">{{ TEXT.modules.feeContracts.lastPaid }}</span>
<span class="date-value">{{ displayDate(scope.row.last_paid_date) }}</span>
</div>
<div class="date-line">
<span class="date-label">{{ TEXT.modules.feeContracts.lastVerified }}</span>
<span class="date-value">{{ displayDate(scope.row.last_verified_date) }}</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="100">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="!canWrite || isInactiveSite(scope.row.center_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="sortedContracts.length === 0" :description="TEXT.modules.feeContracts.empty" />
<section class="unified-section">
<el-table
:data="sortedContracts"
style="width: 100%"
@row-click="onRowClick"
:row-class-name="contractRowClass"
:header-cell-style="{ background: '#f8f9fb' }"
>
<el-table-column prop="center_name" :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">
<div class="site-cell">
<span class="site-name">{{ scope.row.center_name || TEXT.common.fallback }}</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.contractAmount" width="140" align="right">
<template #default="scope">
<span class="amount-text">{{ formatAmountWan(scope.row.contract_amount) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.caseProgress" min-width="140" align="center">
<template #default="scope">
<el-tag effect="plain" type="info">
{{ displayCases(scope.row.contract_cases, scope.row.actual_cases) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.paidSummary" min-width="200">
<template #default="scope">
<div class="summary-line">
<span>{{ TEXT.modules.feeContracts.paidTotal }}</span>
<span class="amount-highlight success">{{ formatAmountWan(scope.row.paid_total) }}</span>
</div>
<div class="summary-line">
<span>{{ TEXT.modules.feeContracts.unpaidBalance }}</span>
<span class="amount-highlight warning" v-if="scope.row.unpaid_balance > 0">{{ formatAmountWan(scope.row.unpaid_balance) }}</span>
<span class="amount-muted" v-else>0.00</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.verifySummary" min-width="200">
<template #default="scope">
<div class="summary-line">
<span>{{ TEXT.modules.feeContracts.verifiedTotal }}</span>
<span class="amount-highlight info">{{ formatAmountWan(scope.row.verified_total) }}</span>
</div>
<div class="summary-line">
<span>{{ TEXT.modules.feeContracts.unverifiedBalance }}</span>
<span class="amount-highlight warning" v-if="scope.row.unverified_balance > 0">{{ formatAmountWan(scope.row.unverified_balance) }}</span>
<span class="amount-muted" v-else>0.00</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeContracts.recentDates" min-width="180">
<template #default="scope">
<div class="date-line">
<span class="date-label">{{ TEXT.modules.feeContracts.lastPaid }}</span>
<span class="date-value">{{ displayDate(scope.row.last_paid_date) }}</span>
</div>
<div class="date-line">
<span class="date-label">{{ TEXT.modules.feeContracts.lastVerified }}</span>
<span class="date-value">{{ displayDate(scope.row.last_verified_date) }}</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="100">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="!canWrite || isInactiveSite(scope.row.center_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="sortedContracts.length === 0" :description="TEXT.modules.feeContracts.empty" />
</section>
</el-card>
</div>
</template>
@@ -347,14 +347,15 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 0;
}
.main-content-card :deep(.el-card__body) {
padding: 20px;
padding: 0;
}
.filter-container {
margin-bottom: 20px;
margin-bottom: 0;
}
.filter-form {
@@ -1,15 +1,5 @@
<template>
<div class="page">
<div class="actions">
<el-button type="primary" :disabled="!canWrite || isReadOnly" @click="goEdit" class="header-action-btn copy-btn">
<el-icon class="el-icon--left"><Edit /></el-icon>
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack" class="header-action-btn">
{{ TEXT.common.actions.back }}
</el-button>
</div>
<StateError v-if="errorMessage" :description="errorMessage">
<template #action>
<el-button type="primary" @click="load">{{ TEXT.common.actions.retry }}</el-button>
@@ -19,16 +9,23 @@
<StateLoading v-else-if="loading" :rows="6" />
<template v-else>
<el-card class="detail-card" shadow="never">
<el-card class="detail-card unified-shell" shadow="never">
<template #header>
<div class="card-header">
<span class="header-title">{{ TEXT.common.labels.basicInfo }}</span>
<div class="status-badges">
<div class="status-badges actions">
<el-tag v-if="detail.is_paid" type="success" effect="dark" size="default">{{ TEXT.modules.feeSpecials.isPaid }}</el-tag>
<el-tag v-else type="info" effect="plain" size="default">{{ TEXT.modules.feeSpecials.unpaid }}</el-tag>
<el-tag v-if="detail.is_verified" type="info" effect="dark" size="default">{{ TEXT.modules.feeSpecials.isVerified }}</el-tag>
<el-tag v-else type="info" effect="plain" size="default">{{ TEXT.modules.feeSpecials.unverified }}</el-tag>
<el-button type="primary" :disabled="!canWrite || isReadOnly" @click="goEdit" class="header-action-btn copy-btn">
<el-icon class="el-icon--left"><Edit /></el-icon>
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack" class="header-action-btn">
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
</template>
@@ -65,7 +62,7 @@
</el-descriptions>
</el-card>
<el-card class="section-card" shadow="never">
<el-card class="section-card unified-shell" shadow="never">
<template #header>
<div class="card-header">
<span class="header-title">{{ TEXT.modules.feeSpecials.attachmentTitle }}</span>
@@ -216,26 +213,7 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: var(--ctms-text-main);
}
.page-subtitle {
margin: 6px 0 0;
font-size: 14px;
color: var(--ctms-text-secondary);
gap: 0;
}
.actions {
+12 -42
View File
@@ -1,15 +1,5 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.feeSpecials.editTitle : TEXT.modules.feeSpecials.newTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.feeSpecials.formSubtitle }}</p>
</div>
<div class="actions">
<el-button @click="goBack" class="header-action-btn">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<StateError v-if="errorMessage" :description="errorMessage">
<template #action>
<el-button type="primary" @click="loadDetail">{{ TEXT.common.actions.retry }}</el-button>
@@ -20,10 +10,11 @@
<template v-else>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="top">
<el-card class="form-card" shadow="never">
<el-card class="form-card unified-shell" shadow="never">
<template #header>
<div class="card-header">
<div class="card-header header-actions-row">
<span class="header-title">{{ TEXT.common.labels.basicInfo }}</span>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</template>
<el-row :gutter="24">
@@ -65,7 +56,7 @@
</el-row>
</el-card>
<el-card class="form-card section-margin" shadow="never">
<el-card class="form-card section-margin unified-shell" shadow="never">
<template #header>
<div class="card-header">
<span class="header-title">{{ TEXT.common.fields.status }}</span>
@@ -116,7 +107,7 @@
</div>
</el-card>
<el-card class="form-card section-margin" shadow="never">
<el-card class="form-card section-margin unified-shell" shadow="never">
<template #header>
<div class="card-header">
<span class="header-title">{{ TEXT.modules.feeSpecials.attachmentTitle }}</span>
@@ -389,37 +380,10 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 24px;
max-width: 1000px;
margin: 0 auto;
gap: 0;
width: 100%;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: var(--ctms-text-main);
}
.page-subtitle {
margin: 6px 0 0;
font-size: 14px;
color: var(--ctms-text-secondary);
}
.header-action-btn {
height: 36px;
border-radius: 8px;
padding: 0 16px;
}
.form-card {
border-radius: 12px;
border: 1px solid var(--ctms-border-color);
@@ -434,6 +398,12 @@ onMounted(async () => {
padding: 4px 0;
}
.header-actions-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
font-size: 16px;
font-weight: 600;
+83 -73
View File
@@ -1,6 +1,5 @@
<template>
<div class="page">
<div class="overview">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
@@ -45,8 +44,16 @@
</el-row>
</div>
<el-card shadow="never" class="main-content-card" v-if="!errorMessage && !loading">
<div class="filter-container">
<StateError v-if="errorMessage" :description="errorMessage">
<template #action>
<el-button type="primary" @click="load">{{ TEXT.common.actions.retry }}</el-button>
</template>
</StateError>
<StateLoading v-else-if="loading" :rows="6" />
<el-card shadow="never" class="main-content-card unified-shell" v-else>
<div class="filter-container unified-action-bar">
<el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="" class="filter-item">
<el-select v-model="filters.centerId" clearable :placeholder="TEXT.common.fields.site" class="filter-select">
@@ -69,7 +76,7 @@
<el-option v-for="option in categoryOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item label="" class="filter-item">
<el-form-item label="" class="filter-item">
<el-date-picker
v-model="filters.dateRange"
type="daterange"
@@ -90,73 +97,75 @@
</el-button>
</el-form>
</div>
<el-table
:data="sortedExpenses"
style="width: 100%"
@row-click="onRowClick"
:header-cell-style="{ background: '#f8f9fb' }"
:row-class-name="expenseRowClass"
>
<el-table-column :label="TEXT.common.fields.occurDate" width="140">
<template #default="scope">
<span class="date-text">{{ displayDate(scope.row.happen_date) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">
<span class="site-text">{{ scope.row.center_name || TEXT.common.fallback }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.category" width="140">
<template #default="scope">
<el-tag size="small" :type="getCategoryType(scope.row.category)" effect="light" class="category-tag">
{{ displayEnum(TEXT.enums.feeSpecialCategory, scope.row.category) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.amount" width="110" align="right">
<template #default="scope">
<span class="amount-text">{{ formatAmount(scope.row.amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="description" :label="TEXT.common.fields.remark" min-width="200" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.description || TEXT.common.fallback }}
</template>
</el-table-column>
<el-table-column width="140" align="center" :label="TEXT.common.fields.status">
<template #default="scope">
<el-tooltip :content="scope.row.is_paid ? `${TEXT.modules.feeSpecials.isPaid} (${displayDate(scope.row.paid_date)})` : TEXT.modules.feeSpecials.unpaid" placement="top">
<el-icon :class="scope.row.is_paid ? 'status-icon success' : 'status-icon muted'"><Wallet /></el-icon>
</el-tooltip>
<el-tooltip :content="scope.row.is_verified ? `${TEXT.modules.feeSpecials.isVerified} (${displayDate(scope.row.verified_date)})` : TEXT.modules.feeSpecials.unverified" placement="top">
<el-icon :class="scope.row.is_verified ? 'status-icon info' : 'status-icon muted'"><CircleCheck /></el-icon>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeSpecials.attachmentCount" width="120" align="center">
<template #default="scope">
<el-tag v-if="scope.row.attachments_count > 0" effect="plain" type="info" round size="small">
{{ scope.row.attachments_count }}
</el-tag>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="!canWrite || isInactiveSite(scope.row.center_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="sortedExpenses.length === 0" :description="TEXT.modules.feeSpecials.empty" />
<section class="unified-section">
<el-table
:data="sortedExpenses"
style="width: 100%"
@row-click="onRowClick"
:header-cell-style="{ background: '#f8f9fb' }"
:row-class-name="expenseRowClass"
>
<el-table-column :label="TEXT.common.fields.occurDate" width="140">
<template #default="scope">
<span class="date-text">{{ displayDate(scope.row.happen_date) }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">
<span class="site-text">{{ scope.row.center_name || TEXT.common.fallback }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.category" width="140">
<template #default="scope">
<el-tag size="small" :type="getCategoryType(scope.row.category)" effect="light" class="category-tag">
{{ displayEnum(TEXT.enums.feeSpecialCategory, scope.row.category) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.amount" width="110" align="right">
<template #default="scope">
<span class="amount-text">{{ formatAmount(scope.row.amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="description" :label="TEXT.common.fields.remark" min-width="200" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.description || TEXT.common.fallback }}
</template>
</el-table-column>
<el-table-column width="140" align="center" :label="TEXT.common.fields.status">
<template #default="scope">
<el-tooltip :content="scope.row.is_paid ? `${TEXT.modules.feeSpecials.isPaid} (${displayDate(scope.row.paid_date)})` : TEXT.modules.feeSpecials.unpaid" placement="top">
<el-icon :class="scope.row.is_paid ? 'status-icon success' : 'status-icon muted'"><Wallet /></el-icon>
</el-tooltip>
<el-tooltip :content="scope.row.is_verified ? `${TEXT.modules.feeSpecials.isVerified} (${displayDate(scope.row.verified_date)})` : TEXT.modules.feeSpecials.unverified" placement="top">
<el-icon :class="scope.row.is_verified ? 'status-icon info' : 'status-icon muted'"><CircleCheck /></el-icon>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="TEXT.modules.feeSpecials.attachmentCount" width="120" align="center">
<template #default="scope">
<el-tag v-if="scope.row.attachments_count > 0" effect="plain" type="info" round size="small">
{{ scope.row.attachments_count }}
</el-tag>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="!canWrite || isInactiveSite(scope.row.center_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="sortedExpenses.length === 0" :description="TEXT.modules.feeSpecials.empty" />
</section>
</el-card>
</div>
</template>
@@ -343,14 +352,15 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 0;
}
.main-content-card :deep(.el-card__body) {
padding: 20px;
padding: 0;
}
.filter-container {
margin-bottom: 20px;
margin-bottom: 0;
}
.filter-form {
+29 -53
View File
@@ -1,35 +1,34 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ TEXT.modules.financeContracts.detailTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.financeContracts.detailSubtitle }}</p>
</div>
<div class="actions">
<el-button type="primary" :disabled="isReadOnly" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
<div class="unified-shell">
<section class="unified-section" v-loading="loading">
<div class="unified-section-header">
<div class="ctms-section-title">{{ TEXT.modules.financeContracts.detailTitle }}</div>
<div class="ctms-section-actions">
<el-button type="primary" :disabled="isReadOnly" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.contractNo">{{ detail.contract_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.signedDate">{{ displayDate(detail.signed_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.amount">
{{ detail.amount }} {{ detail.currency || TEXT.common.fields.currencyDefault }}
</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</section>
<section class="unified-section">
<AttachmentList
v-if="contractId"
:study-id="studyId"
entity-type="finance_contract"
:entity-id="contractId"
:readonly="isReadOnly"
/>
</section>
</div>
<el-card v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.contractNo">{{ detail.contract_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.signedDate">{{ displayDate(detail.signed_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.amount">
{{ detail.amount }} {{ detail.currency || TEXT.common.fields.currencyDefault }}
</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</el-card>
<AttachmentList
v-if="contractId"
:study-id="studyId"
entity-type="finance_contract"
:entity-id="contractId"
:readonly="isReadOnly"
/>
</div>
</template>
@@ -112,29 +111,6 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.actions {
display: flex;
gap: 8px;
gap: 0;
}
</style>
+48 -71
View File
@@ -1,54 +1,53 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.financeContracts.editTitle : TEXT.modules.financeContracts.newTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.financeContracts.formSubtitle }}</p>
</div>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
<div class="unified-shell">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ isEdit ? TEXT.modules.financeContracts.editTitle : TEXT.modules.financeContracts.newTitle }}</div>
<div class="ctms-section-actions">
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-form ref="formRef" :model="form" label-width="110px">
<el-form-item :label="TEXT.common.fields.site" prop="site_name" required>
<el-input v-model="form.site_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.contractNo" prop="contract_no" required>
<el-input v-model="form.contract_no" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.contractNo" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.signedDate">
<el-date-picker v-model="form.signed_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.amount" prop="amount" required>
<el-input-number v-model="form.amount" :disabled="isReadOnly" :min="0" :precision="2" :step="1000" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.currency">
<el-select v-model="form.currency" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select">
<el-option label="CNY" value="CNY" />
<el-option label="USD" value="USD" />
<el-option label="EUR" value="EUR" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.remark">
<el-input v-model="form.remark" :disabled="isReadOnly" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</section>
<section class="unified-section">
<AttachmentList
v-if="isEdit && contractId"
:study-id="studyId"
entity-type="finance_contract"
:entity-id="contractId"
:readonly="isReadOnly"
/>
<StateEmpty v-else :description="TEXT.modules.financeContracts.uploadHint" />
</section>
</div>
<el-card>
<el-form ref="formRef" :model="form" label-width="110px">
<el-form-item :label="TEXT.common.fields.site" prop="site_name" required>
<el-input v-model="form.site_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.contractNo" prop="contract_no" required>
<el-input v-model="form.contract_no" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.contractNo" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.signedDate">
<el-date-picker v-model="form.signed_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.amount" prop="amount" required>
<el-input-number v-model="form.amount" :disabled="isReadOnly" :min="0" :precision="2" :step="1000" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.currency">
<el-select v-model="form.currency" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select">
<el-option label="CNY" value="CNY" />
<el-option label="USD" value="USD" />
<el-option label="EUR" value="EUR" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.remark">
<el-input v-model="form.remark" :disabled="isReadOnly" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</el-card>
<AttachmentList
v-if="isEdit && contractId"
:study-id="studyId"
entity-type="finance_contract"
:entity-id="contractId"
:readonly="isReadOnly"
/>
<el-card v-else class="hint-card">
<StateEmpty :description="TEXT.modules.financeContracts.uploadHint" />
</el-card>
</div>
</template>
@@ -170,28 +169,6 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.hint-card {
padding: 12px;
gap: 0;
}
</style>
+28 -52
View File
@@ -1,34 +1,33 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ TEXT.modules.financeSpecials.detailTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.financeSpecials.detailSubtitle }}</p>
</div>
<div class="actions">
<el-button type="primary" :disabled="isReadOnly" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
<div class="unified-shell">
<section class="unified-section" v-loading="loading">
<div class="unified-section-header">
<div class="ctms-section-title">{{ TEXT.modules.financeSpecials.detailTitle }}</div>
<div class="ctms-section-actions">
<el-button type="primary" :disabled="isReadOnly" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.type">{{ displayEnum(TEXT.enums.financeSpecialType, detail.fee_type) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.occurDate">{{ displayDate(detail.occur_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.staff">{{ detail.staff_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.amount">{{ detail.amount || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</section>
<section class="unified-section">
<AttachmentList
v-if="specialId"
:study-id="studyId"
entity-type="finance_special"
:entity-id="specialId"
:readonly="isReadOnly"
/>
</section>
</div>
<el-card v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.type">{{ displayEnum(TEXT.enums.financeSpecialType, detail.fee_type) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.occurDate">{{ displayDate(detail.occur_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.staff">{{ detail.staff_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.amount">{{ detail.amount || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</el-card>
<AttachmentList
v-if="specialId"
:study-id="studyId"
entity-type="finance_special"
:entity-id="specialId"
:readonly="isReadOnly"
/>
</div>
</template>
@@ -111,29 +110,6 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.actions {
display: flex;
gap: 8px;
gap: 0;
}
</style>
+49 -72
View File
@@ -1,55 +1,54 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.financeSpecials.editTitle : TEXT.modules.financeSpecials.newTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.financeSpecials.subtitle }}</p>
</div>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
<div class="unified-shell">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ isEdit ? TEXT.modules.financeSpecials.editTitle : TEXT.modules.financeSpecials.newTitle }}</div>
<div class="ctms-section-actions">
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-form :model="form" label-width="110px">
<el-form-item :label="TEXT.common.fields.site" required>
<el-input v-model="form.site_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.type" required>
<el-select v-model="form.fee_type" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select">
<el-option :label="TEXT.enums.financeSpecialType.TRAVEL" value="TRAVEL" />
<el-option :label="TEXT.enums.financeSpecialType.HOTEL" value="HOTEL" />
<el-option :label="TEXT.enums.financeSpecialType.TRANSPORT" value="TRANSPORT" />
<el-option :label="TEXT.enums.financeSpecialType.OTHER" value="OTHER" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.amount" required>
<el-input-number v-model="form.amount" :disabled="isReadOnly" :min="0" :precision="2" :step="100" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.occurDate">
<el-date-picker v-model="form.occur_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.staff">
<el-input v-model="form.staff_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.staff" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.remark">
<el-input v-model="form.remark" :disabled="isReadOnly" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</section>
<section class="unified-section">
<AttachmentList
v-if="isEdit && specialId"
:study-id="studyId"
entity-type="finance_special"
:entity-id="specialId"
:readonly="isReadOnly"
/>
<StateEmpty v-else :description="TEXT.modules.financeSpecials.uploadHint" />
</section>
</div>
<el-card>
<el-form :model="form" label-width="110px">
<el-form-item :label="TEXT.common.fields.site" required>
<el-input v-model="form.site_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.type" required>
<el-select v-model="form.fee_type" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select">
<el-option :label="TEXT.enums.financeSpecialType.TRAVEL" value="TRAVEL" />
<el-option :label="TEXT.enums.financeSpecialType.HOTEL" value="HOTEL" />
<el-option :label="TEXT.enums.financeSpecialType.TRANSPORT" value="TRANSPORT" />
<el-option :label="TEXT.enums.financeSpecialType.OTHER" value="OTHER" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.amount" required>
<el-input-number v-model="form.amount" :disabled="isReadOnly" :min="0" :precision="2" :step="100" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.occurDate">
<el-date-picker v-model="form.occur_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.staff">
<el-input v-model="form.staff_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.staff" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.remark">
<el-input v-model="form.remark" :disabled="isReadOnly" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</el-card>
<AttachmentList
v-if="isEdit && specialId"
:study-id="studyId"
entity-type="finance_special"
:entity-id="specialId"
:readonly="isReadOnly"
/>
<el-card v-else class="hint-card">
<StateEmpty :description="TEXT.modules.financeSpecials.uploadHint" />
</el-card>
</div>
</template>
@@ -171,28 +170,6 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.hint-card {
padding: 12px;
gap: 0;
}
</style>
+62 -69
View File
@@ -1,8 +1,8 @@
<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">
<el-select v-model="filters.center_id" clearable :placeholder="TEXT.common.fields.site" class="filter-select">
@@ -42,66 +42,68 @@
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
</el-form-item>
<div class="filter-spacer"></div>
<el-button type="primary" @click="goNew" class="header-action-btn">
<el-button type="primary" @click="goNew">
<el-icon class="el-icon--left"><Plus /></el-icon>
{{ TEXT.modules.drugShipments.newTitle }}
</el-button>
</el-form>
</div>
<el-table
:data="sortedItems"
v-loading="loading"
style="width: 100%"
:row-class-name="shipmentRowClass"
@row-click="onRowClick"
>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">{{ scope.row.site_name || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="direction" :label="TEXT.common.fields.direction" width="120">
<template #default="scope">
<el-tag :class="['type-tag', scope.row.direction === 'SEND' ? 'type-send' : 'type-return']" effect="light">
{{ displayEnum(TEXT.enums.shipmentDirection, scope.row.direction) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="ship_date" :label="TEXT.common.fields.shipDate" width="140">
<template #default="scope">{{ displayDate(scope.row.ship_date) }}</template>
</el-table-column>
<el-table-column prop="receive_date" :label="TEXT.common.fields.receiveDate" width="140">
<template #default="scope">{{ displayDate(scope.row.receive_date) }}</template>
</el-table-column>
<el-table-column prop="quantity" :label="TEXT.common.fields.quantity" width="120" align="right">
<template #default="scope">{{ typeof scope.row.quantity === "number" ? scope.row.quantity : TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="batch_no" :label="TEXT.common.fields.batchNo" min-width="140">
<template #default="scope">{{ scope.row.batch_no || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" width="140">
<template #default="scope">
<el-tag :type="statusType(scope.row.status)" effect="plain" round size="small" class="status-tag">
{{ displayEnum(TEXT.enums.shipmentStatus, scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" :label="TEXT.common.fields.remark" min-width="160" show-overflow-tooltip>
<template #default="scope">{{ scope.row.remark || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120" align="center">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.center_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.drugShipments.empty" />
<div class="unified-section table-section">
<el-table
:data="sortedItems"
v-loading="loading"
style="width: 100%"
:row-class-name="shipmentRowClass"
@row-click="onRowClick"
>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">{{ scope.row.site_name || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="direction" :label="TEXT.common.fields.direction" width="120">
<template #default="scope">
<el-tag :class="['type-tag', scope.row.direction === 'SEND' ? 'type-send' : 'type-return']" effect="light">
{{ displayEnum(TEXT.enums.shipmentDirection, scope.row.direction) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="ship_date" :label="TEXT.common.fields.shipDate" width="140">
<template #default="scope">{{ displayDate(scope.row.ship_date) }}</template>
</el-table-column>
<el-table-column prop="receive_date" :label="TEXT.common.fields.receiveDate" width="140">
<template #default="scope">{{ displayDate(scope.row.receive_date) }}</template>
</el-table-column>
<el-table-column prop="quantity" :label="TEXT.common.fields.quantity" width="120" align="right">
<template #default="scope">{{ typeof scope.row.quantity === "number" ? scope.row.quantity : TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="batch_no" :label="TEXT.common.fields.batchNo" min-width="140">
<template #default="scope">{{ scope.row.batch_no || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" width="140">
<template #default="scope">
<el-tag :type="statusType(scope.row.status)" effect="plain" round size="small" class="status-tag">
{{ displayEnum(TEXT.enums.shipmentStatus, scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" :label="TEXT.common.fields.remark" min-width="160" show-overflow-tooltip>
<template #default="scope">{{ scope.row.remark || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120" align="center">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.center_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.drugShipments.empty" />
</div>
</el-card>
</div>
</template>
@@ -247,14 +249,6 @@ onMounted(async () => {
flex-direction: column;
}
.main-content-card :deep(.el-card__body) {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.filter-form {
display: flex;
width: 100%;
@@ -291,10 +285,9 @@ onMounted(async () => {
flex: 1;
}
.header-action-btn {
height: 36px;
padding: 0 20px;
border-radius: 8px;
.table-section {
padding-top: 12px;
padding-bottom: 12px;
}
.type-tag {
+13
View File
@@ -0,0 +1,13 @@
<template>
<ModulePlaceholder
:title="TEXT.modules.etmf.title"
:subtitle="TEXT.modules.etmf.subtitle"
:list-title="TEXT.modules.etmf.listTitle"
:empty-description="TEXT.modules.etmf.emptyDescription"
/>
</template>
<script setup lang="ts">
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
import { TEXT } from "../../locales";
</script>
+48 -74
View File
@@ -1,8 +1,7 @@
<template>
<div class="page">
<el-card class="filter-card" shadow="never">
<div class="filter-row">
<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-input
@@ -24,54 +23,53 @@
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
</el-form-item>
<div class="filter-spacer"></div>
<el-button type="primary" @click="goNew" class="header-action-btn">
<el-button type="primary" @click="goNew">
{{ TEXT.modules.financeContracts.newTitle }}
</el-button>
</el-form>
</div>
</el-card>
<el-card>
<el-table
:data="sortedItems"
v-loading="loading"
style="width: 100%"
class="ctms-table"
@row-click="onRowClick"
:row-class-name="contractRowClass"
>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
<el-table-column prop="contract_no" :label="TEXT.common.fields.contractNo" min-width="160" />
<el-table-column prop="signed_date" :label="TEXT.common.fields.signedDate" width="140">
<template #default="scope">
{{ displayDate(scope.row.signed_date) }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.amount" width="160">
<template #default="scope">
{{ scope.row.amount }} {{ scope.row.currency || TEXT.common.fields.currencyDefault }}
</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
<template #default="scope">
{{ displayDateTime(scope.row.updated_at) }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.site_name)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.financeContracts.empty" />
<div class="unified-section table-section">
<el-table
:data="sortedItems"
v-loading="loading"
style="width: 100%"
class="ctms-table"
@row-click="onRowClick"
:row-class-name="contractRowClass"
>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
<el-table-column prop="contract_no" :label="TEXT.common.fields.contractNo" min-width="160" />
<el-table-column prop="signed_date" :label="TEXT.common.fields.signedDate" width="140">
<template #default="scope">
{{ displayDate(scope.row.signed_date) }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.amount" width="160">
<template #default="scope">
{{ scope.row.amount }} {{ scope.row.currency || TEXT.common.fields.currencyDefault }}
</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
<template #default="scope">
{{ displayDateTime(scope.row.updated_at) }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.site_name)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.financeContracts.empty" />
</div>
</el-card>
</div>
</template>
@@ -185,17 +183,6 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 12px;
}
.filter-card {
border: none;
background-color: transparent;
padding: 0;
}
.filter-card :deep(.el-card__body) {
padding: 0;
}
.filter-form {
@@ -203,18 +190,6 @@ onMounted(async () => {
width: 100%;
gap: 12px;
align-items: center;
background: white;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid var(--ctms-border-color);
box-sizing: border-box;
}
.filter-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.filter-item-form {
@@ -230,10 +205,9 @@ onMounted(async () => {
flex: 1;
}
.header-action-btn {
height: 36px;
padding: 0 20px;
border-radius: 8px;
.table-section {
padding-top: 12px;
padding-bottom: 12px;
}
+53 -79
View File
@@ -1,8 +1,7 @@
<template>
<div class="page">
<el-card class="filter-card" shadow="never">
<div class="filter-row">
<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-input
@@ -29,59 +28,58 @@
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
</el-form-item>
<div class="filter-spacer"></div>
<el-button type="primary" @click="goNew" class="header-action-btn">
<el-button type="primary" @click="goNew">
{{ TEXT.modules.financeSpecials.newTitle }}
</el-button>
</el-form>
</div>
</el-card>
<el-card>
<el-table
:data="sortedItems"
v-loading="loading"
style="width: 100%"
class="ctms-table"
@row-click="onRowClick"
:row-class-name="specialRowClass"
>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
<el-table-column prop="fee_type" :label="TEXT.common.fields.type" width="120">
<template #default="scope">
{{ displayEnum(TEXT.enums.financeSpecialType, scope.row.fee_type) }}
</template>
</el-table-column>
<el-table-column prop="occur_date" :label="TEXT.common.fields.occurDate" width="140">
<template #default="scope">
{{ displayDate(scope.row.occur_date) }}
</template>
</el-table-column>
<el-table-column prop="staff_name" :label="TEXT.common.fields.staff" min-width="120" />
<el-table-column :label="TEXT.common.fields.amount" width="140">
<template #default="scope">
{{ scope.row.amount }}
</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
<template #default="scope">
{{ displayDateTime(scope.row.updated_at) }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.site_name)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.financeSpecials.empty" />
<div class="unified-section table-section">
<el-table
:data="sortedItems"
v-loading="loading"
style="width: 100%"
class="ctms-table"
@row-click="onRowClick"
:row-class-name="specialRowClass"
>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
<el-table-column prop="fee_type" :label="TEXT.common.fields.type" width="120">
<template #default="scope">
{{ displayEnum(TEXT.enums.financeSpecialType, scope.row.fee_type) }}
</template>
</el-table-column>
<el-table-column prop="occur_date" :label="TEXT.common.fields.occurDate" width="140">
<template #default="scope">
{{ displayDate(scope.row.occur_date) }}
</template>
</el-table-column>
<el-table-column prop="staff_name" :label="TEXT.common.fields.staff" min-width="120" />
<el-table-column :label="TEXT.common.fields.amount" width="140">
<template #default="scope">
{{ scope.row.amount }}
</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
<template #default="scope">
{{ displayDateTime(scope.row.updated_at) }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.site_name)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.financeSpecials.empty" />
</div>
</el-card>
</div>
</template>
@@ -194,17 +192,6 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 12px;
}
.filter-card {
border: none;
background-color: transparent;
padding: 0;
}
.filter-card :deep(.el-card__body) {
padding: 0;
}
.filter-form {
@@ -212,18 +199,6 @@ onMounted(async () => {
width: 100%;
gap: 12px;
align-items: center;
background: white;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid var(--ctms-border-color);
box-sizing: border-box;
}
.filter-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.filter-item-form {
@@ -243,10 +218,9 @@ onMounted(async () => {
flex: 1;
}
.header-action-btn {
height: 36px;
padding: 0 20px;
border-radius: 8px;
.table-section {
padding-top: 12px;
padding-bottom: 12px;
}
</style>
+37 -44
View File
@@ -1,8 +1,8 @@
<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-input
@@ -24,40 +24,42 @@
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
</el-form-item>
<div class="filter-spacer"></div>
<el-button type="primary" @click="goNew" class="header-action-btn">
<el-button type="primary" @click="goNew">
{{ TEXT.modules.knowledgeNotes.newTitle }}
</el-button>
</el-form>
</div>
<el-table
:data="sortedItems"
v-loading="loading"
style="width: 100%"
class="ctms-table"
@row-click="onRowClick"
:row-class-name="noteRowClass"
>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
<el-table-column prop="title" :label="TEXT.common.fields.title" min-width="200" />
<el-table-column prop="level" :label="TEXT.common.fields.level" width="120" />
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
<template #default="scope">{{ displayDateTime(scope.row.updated_at) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.site_name)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.knowledgeNotes.empty" />
<div class="unified-section table-section">
<el-table
:data="sortedItems"
v-loading="loading"
style="width: 100%"
class="ctms-table"
@row-click="onRowClick"
:row-class-name="noteRowClass"
>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
<el-table-column prop="title" :label="TEXT.common.fields.title" min-width="200" />
<el-table-column prop="level" :label="TEXT.common.fields.level" width="120" />
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" width="180">
<template #default="scope">{{ displayDateTime(scope.row.updated_at) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.site_name)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.knowledgeNotes.empty" />
</div>
</el-card>
</div>
</template>
@@ -173,14 +175,6 @@ onMounted(async () => {
flex-direction: column;
}
.main-content-card :deep(.el-card__body) {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.filter-form {
display: flex;
width: 100%;
@@ -201,10 +195,9 @@ onMounted(async () => {
flex: 1;
}
.header-action-btn {
height: 36px;
padding: 0 20px;
border-radius: 8px;
.table-section {
padding-top: 12px;
padding-bottom: 12px;
}
</style>
@@ -0,0 +1,13 @@
<template>
<ModulePlaceholder
:title="TEXT.modules.materialEquipment.title"
:subtitle="TEXT.modules.materialEquipment.subtitle"
:list-title="TEXT.modules.materialEquipment.listTitle"
:empty-description="TEXT.modules.materialEquipment.emptyDescription"
/>
</template>
<script setup lang="ts">
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
import { TEXT } from "../../locales";
</script>
+13
View File
@@ -0,0 +1,13 @@
<template>
<ModulePlaceholder
:title="TEXT.modules.materialOthers.title"
:subtitle="TEXT.modules.materialOthers.subtitle"
:list-title="TEXT.modules.materialOthers.listTitle"
:empty-description="TEXT.modules.materialOthers.emptyDescription"
/>
</template>
<script setup lang="ts">
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
import { TEXT } from "../../locales";
</script>
@@ -0,0 +1,13 @@
<template>
<ModulePlaceholder
:title="TEXT.modules.monitoringAudit.title"
:subtitle="TEXT.modules.monitoringAudit.subtitle"
:list-title="TEXT.modules.monitoringAudit.listTitle"
:empty-description="TEXT.modules.monitoringAudit.emptyDescription"
/>
</template>
<script setup lang="ts">
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
import { TEXT } from "../../locales";
</script>
@@ -0,0 +1,13 @@
<template>
<ModulePlaceholder
:title="TEXT.modules.projectMilestones.title"
:subtitle="TEXT.modules.projectMilestones.subtitle"
:list-title="TEXT.modules.projectMilestones.listTitle"
:empty-description="TEXT.modules.projectMilestones.emptyDescription"
/>
</template>
<script setup lang="ts">
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
import { TEXT } from "../../locales";
</script>
+42 -85
View File
@@ -1,19 +1,13 @@
<template>
<div class="page">
<div v-if="study.currentStudy" class="page-body">
<div class="page-header">
<div>
<h1 class="page-title">项目概览</h1>
</div>
<div class="header-meta">
<el-tag v-if="usingDemo" effect="plain" type="info" class="demo-tag">示例数据</el-tag>
<div class="updated-at">更新{{ nowLabel }}</div>
<el-button size="small" @click="loadOverview">刷新</el-button>
</div>
</div>
<el-card>
<template #header>
<div class="unified-shell">
<section class="unified-section">
<div class="overview-meta-inline">
<el-tag v-if="usingDemo" effect="plain" type="info" class="demo-tag">示例数据</el-tag>
<div class="updated-at">更新{{ nowLabel }}</div>
<el-button size="small" @click="loadOverview">刷新</el-button>
</div>
<div class="card-header">
<div>
<div class="card-title">中心整体进度</div>
@@ -25,25 +19,22 @@
<span class="legend-item"><span class="legend-dot blocked"></span>阻塞/延期</span>
</div>
</div>
</template>
<StateLoading v-if="loading" :rows="6" />
<StateEmpty
v-else-if="centers.length === 0"
title="暂无中心进度"
description="当前项目未配置中心或暂无进度数据"
/>
<div v-else class="progress-list">
<CenterProgressRow
v-for="(center, index) in centers"
:key="center.center_id || center.center_name || index"
:center="center"
<StateLoading v-if="loading" :rows="6" />
<StateEmpty
v-else-if="centers.length === 0"
title="暂无中心进度"
description="当前项目未配置中心或暂无进度数据"
/>
</div>
</el-card>
<div v-else class="progress-list">
<CenterProgressRow
v-for="(center, index) in centers"
:key="center.center_id || center.center_name || index"
:center="center"
/>
</div>
</section>
<el-card>
<template #header>
<section class="unified-section">
<div class="card-header">
<div>
<div class="card-title">入组进度</div>
@@ -56,14 +47,14 @@
<el-radio-button label="month">按月份</el-radio-button>
</el-radio-group>
</div>
</template>
<EnrollmentBarChart
:mode="chartMode"
:items="chartItems"
:loading="loading"
:empty-text="chartEmptyText"
/>
</el-card>
<EnrollmentBarChart
:mode="chartMode"
:items="chartItems"
:loading="loading"
:empty-text="chartEmptyText"
/>
</section>
</div>
</div>
<StateEmpty v-else :description="TEXT.common.empty.selectProject" />
</div>
@@ -198,55 +189,13 @@ watch(
.page {
display: flex;
flex-direction: column;
gap: 16px;
gap: 0;
}
.page-body {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 16px;
flex-wrap: wrap;
}
.page-title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: var(--ctms-text-main);
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.study-meta {
margin: 6px 0 0;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--ctms-text-secondary);
}
.study-divider {
opacity: 0.3;
}
.header-meta {
display: flex;
align-items: center;
gap: 10px;
color: var(--ctms-text-secondary);
font-size: 12px;
gap: 0;
}
.demo-tag {
@@ -258,6 +207,14 @@ watch(
white-space: nowrap;
}
.overview-meta-inline {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.card-header {
display: flex;
align-items: center;
@@ -268,8 +225,8 @@ watch(
.card-title {
font-size: 15px;
font-weight: 600;
color: var(--ctms-text-main);
font-weight: 700;
color: #0f2345;
}
.card-subtitle {
@@ -322,7 +279,7 @@ watch(
.progress-list {
display: flex;
flex-direction: column;
gap: 4px;
gap: 2px;
}
.mode-switch :deep(.el-radio-button__inner) {
@@ -0,0 +1,13 @@
<template>
<ModulePlaceholder
:title="TEXT.modules.riskIssueMonitoringVisits.title"
:subtitle="TEXT.modules.riskIssueMonitoringVisits.subtitle"
:list-title="TEXT.modules.riskIssueMonitoringVisits.listTitle"
:empty-description="TEXT.modules.riskIssueMonitoringVisits.emptyDescription"
/>
</template>
<script setup lang="ts">
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
import { TEXT } from "../../locales";
</script>
+195
View File
@@ -0,0 +1,195 @@
<template>
<div class="page">
<el-card shadow="never" class="main-content-card unified-shell">
<div class="filter-container unified-action-bar">
<el-form :inline="true" class="filter-form">
<el-form-item class="filter-item-form">
<el-input v-model="filters.keyword" :placeholder="TEXT.modules.subjectManagement.screeningNo" clearable class="filter-input-comp">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item class="filter-item-form">
<el-select v-model="filters.siteId" :placeholder="TEXT.common.fields.site" clearable filterable class="filter-select-comp">
<template #prefix>
<el-icon><Location /></el-icon>
</template>
<el-option :label="TEXT.common.labels.all" value="" />
<el-option v-for="site in siteOptions" :key="site.id" :label="site.name" :value="site.id" />
</el-select>
</el-form-item>
<el-form-item class="filter-item-form">
<el-select v-model="filters.status" :placeholder="TEXT.common.fields.status" clearable class="filter-select-comp">
<template #prefix>
<el-icon><CircleCheck /></el-icon>
</template>
<el-option :label="TEXT.common.labels.all" value="" />
<el-option v-for="option in statusOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item class="filter-actions-comp">
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
</el-form-item>
</el-form>
</div>
<div class="unified-section table-section">
<el-table :data="filteredItems" v-loading="loading" style="width: 100%">
<el-table-column prop="subject_no" :label="TEXT.modules.subjectManagement.screeningNo" min-width="160" />
<el-table-column :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">{{ siteMap[scope.row.site_id] || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.subjectStatus, scope.row.status) }}</template>
</el-table-column>
<el-table-column prop="enrollment_date" :label="TEXT.common.fields.enrollmentDate" width="140">
<template #default="scope">{{ displayDate(scope.row.enrollment_date) }}</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" min-width="180">
<template #default="scope">{{ displayDate(scope.row.updated_at) }}</template>
</el-table-column>
<el-table-column :label="TEXT.modules.riskIssuePd.syncStatus" width="120">
<template #default>
<el-tag size="small" type="success" effect="plain">{{ TEXT.modules.riskIssuePd.synced }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button link type="primary" size="small" @click="goSubject(scope.row.id)">
{{ TEXT.modules.riskIssuePd.toSubject }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && filteredItems.length === 0" :description="TEXT.modules.riskIssuePd.emptyDescription" />
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { CircleCheck, Location, Search } from "@element-plus/icons-vue";
import { useStudyStore } from "../../store/study";
import { fetchSubjects } from "../../api/subjects";
import { fetchSites } from "../../api/sites";
import { displayDate, displayEnum } from "../../utils/display";
import StateEmpty from "../../components/StateEmpty.vue";
import { TEXT } from "../../locales";
const router = useRouter();
const study = useStudyStore();
const loading = ref(false);
const items = ref<any[]>([]);
const siteOptions = ref<Array<{ id: string; name: string }>>([]);
const siteMap = ref<Record<string, string>>({});
const filters = ref({
keyword: "",
siteId: study.currentSite?.id || "",
status: "",
});
const statusOptions = Object.entries(TEXT.enums.subjectStatus).map(([value, label]) => ({ value, label }));
const resetFilters = () => {
filters.value.keyword = "";
filters.value.siteId = "";
filters.value.status = "";
};
const loadSites = async (studyId: string) => {
try {
const { data } = await fetchSites(studyId, { limit: 500 });
const list = Array.isArray(data) ? data : data.items || [];
siteOptions.value = list.map((site: any) => ({ id: site.id, name: site.name }));
siteMap.value = list.reduce((acc: Record<string, string>, site: any) => {
acc[site.id] = site.name;
return acc;
}, {});
} catch {
siteOptions.value = [];
siteMap.value = {};
}
};
const load = async () => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
loading.value = true;
try {
await loadSites(studyId);
const { data } = await fetchSubjects(studyId);
items.value = (Array.isArray(data) ? data : data.items || []).sort((a: any, b: any) =>
String(b.updated_at || "").localeCompare(String(a.updated_at || ""))
);
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
} finally {
loading.value = false;
}
};
const filteredItems = computed(() => {
const keyword = filters.value.keyword.trim().toLowerCase();
return items.value.filter((item) => {
const subjectNo = String(item?.subject_no || "").toLowerCase();
if (keyword && !subjectNo.includes(keyword)) return false;
if (filters.value.siteId && item?.site_id !== filters.value.siteId) return false;
if (filters.value.status && item?.status !== filters.value.status) return false;
return true;
});
});
const goSubject = (subjectId: string) => {
if (!subjectId) return;
router.push({ path: `/subjects/${subjectId}`, query: { tab: "pd" } });
};
watch(
() => study.currentSite,
(newSite) => {
filters.value.siteId = newSite?.id || "";
}
);
onMounted(load);
</script>
<style scoped>
.page {
display: flex;
flex-direction: column;
}
.table-section {
padding-top: 12px;
padding-bottom: 12px;
}
.filter-form {
display: flex;
width: 100%;
flex-wrap: wrap;
gap: 0;
}
.filter-item-form {
margin-bottom: 0;
margin-right: 12px;
}
.filter-input-comp {
width: 200px;
}
.filter-select-comp {
width: 170px;
}
.filter-actions-comp {
margin-bottom: 0;
margin-right: 0;
}
</style>
+251
View File
@@ -0,0 +1,251 @@
<template>
<div class="page">
<el-card shadow="never" class="main-content-card unified-shell">
<div class="filter-container unified-action-bar">
<el-form :inline="true" class="filter-form">
<el-form-item class="filter-item-form">
<el-input v-model="filters.keyword" :placeholder="TEXT.common.fields.keyword" clearable class="filter-input-comp">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item class="filter-item-form">
<el-select v-model="filters.siteId" :placeholder="TEXT.common.fields.site" clearable filterable class="filter-select-comp">
<template #prefix>
<el-icon><Location /></el-icon>
</template>
<el-option :label="TEXT.common.labels.all" value="" />
<el-option v-for="site in siteOptions" :key="site.id" :label="site.name" :value="site.id" />
</el-select>
</el-form-item>
<el-form-item class="filter-item-form">
<el-select v-model="filters.status" :placeholder="TEXT.common.fields.status" clearable class="filter-select-comp">
<template #prefix>
<el-icon><CircleCheck /></el-icon>
</template>
<el-option :label="TEXT.common.labels.all" value="" />
<el-option v-for="option in aeStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item class="filter-item-form">
<el-select v-model="filters.seriousness" :placeholder="TEXT.common.fields.seriousness" clearable class="filter-select-comp">
<template #prefix>
<el-icon><Warning /></el-icon>
</template>
<el-option :label="TEXT.common.labels.all" value="" />
<el-option v-for="option in aeSeriousnessOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item class="filter-item-form">
<el-select v-model="filters.aeType" :placeholder="TEXT.common.fields.aeType" clearable class="filter-select-comp">
<template #prefix>
<el-icon><Warning /></el-icon>
</template>
<el-option :label="TEXT.common.labels.all" value="" />
<el-option v-for="option in aeTypeOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item class="filter-actions-comp">
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
</el-form-item>
</el-form>
</div>
<div class="unified-section table-section">
<el-table :data="filteredItems" v-loading="loading" style="width: 100%">
<el-table-column prop="onset_date" :label="TEXT.common.fields.occurDate" width="140">
<template #default="scope">{{ displayDate(scope.row.onset_date) }}</template>
</el-table-column>
<el-table-column prop="subject_no" :label="TEXT.modules.riskIssueSae.subjectNo" min-width="140">
<template #default="scope">{{ subjectMap[scope.row.subject_id] || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.site" min-width="140">
<template #default="scope">{{ siteMap[scope.row.site_id] || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="term" :label="TEXT.common.fields.title" min-width="180" />
<el-table-column prop="seriousness" :label="TEXT.common.fields.seriousness" width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.aeSeriousness, scope.row.seriousness) }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.aeStatus, scope.row.status) }}</template>
</el-table-column>
<el-table-column prop="updated_at" :label="TEXT.common.labels.updatedAt" min-width="180">
<template #default="scope">{{ displayDate(scope.row.updated_at) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button link type="primary" size="small" :disabled="!scope.row.subject_id" @click="goSubject(scope.row.subject_id, 'ae')">
{{ TEXT.modules.riskIssueSae.toSubject }}
</el-button>
</template>
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && filteredItems.length === 0" :description="TEXT.modules.riskIssueSae.emptyDescription" />
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { CircleCheck, Location, Search, Warning } from "@element-plus/icons-vue";
import { useStudyStore } from "../../store/study";
import { fetchAes } from "../../api/aes";
import { fetchSubjects } from "../../api/subjects";
import { fetchSites } from "../../api/sites";
import { displayDate, displayEnum } from "../../utils/display";
import StateEmpty from "../../components/StateEmpty.vue";
import { TEXT } from "../../locales";
const router = useRouter();
const study = useStudyStore();
const loading = ref(false);
const items = ref<any[]>([]);
const siteOptions = ref<Array<{ id: string; name: string }>>([]);
const siteMap = ref<Record<string, string>>({});
const subjectMap = ref<Record<string, string>>({});
const subjectSiteMap = ref<Record<string, string>>({});
const filters = ref({
keyword: "",
siteId: study.currentSite?.id || "",
status: "",
seriousness: "",
aeType: "",
});
const aeStatusOptions = Object.entries(TEXT.enums.aeStatus).map(([value, label]) => ({ value, label }));
const aeSeriousnessOptions = Object.entries(TEXT.enums.aeSeriousness).map(([value, label]) => ({ value, label }));
const aeTypeOptions = [
{ value: "ae", label: TEXT.modules.subjectDetail.aeType.ae },
{ value: "sae", label: TEXT.modules.subjectDetail.aeType.sae },
{ value: "susar", label: TEXT.modules.subjectDetail.aeType.susar },
];
const resetFilters = () => {
filters.value.keyword = "";
filters.value.siteId = "";
filters.value.status = "";
filters.value.seriousness = "";
filters.value.aeType = "";
};
const loadSites = async (studyId: string) => {
try {
const { data } = await fetchSites(studyId, { limit: 500 });
const list = Array.isArray(data) ? data : data.items || [];
siteOptions.value = list.map((site: any) => ({ id: site.id, name: site.name }));
siteMap.value = list.reduce((acc: Record<string, string>, site: any) => {
acc[site.id] = site.name;
return acc;
}, {});
} catch {
siteOptions.value = [];
siteMap.value = {};
}
};
const loadSubjects = async (studyId: string) => {
const { data } = await fetchSubjects(studyId);
const list = Array.isArray(data) ? data : data.items || [];
subjectMap.value = list.reduce((acc: Record<string, string>, item: any) => {
acc[item.id] = item.subject_no;
return acc;
}, {});
subjectSiteMap.value = list.reduce((acc: Record<string, string>, item: any) => {
acc[item.id] = item.site_id;
return acc;
}, {});
};
const load = async () => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
loading.value = true;
try {
await Promise.all([loadSites(studyId), loadSubjects(studyId)]);
const { data } = await fetchAes(studyId);
items.value = (Array.isArray(data) ? data : data.items || []).sort((a: any, b: any) =>
String(b.updated_at || "").localeCompare(String(a.updated_at || ""))
);
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
} finally {
loading.value = false;
}
};
const filteredItems = computed(() => {
const keyword = filters.value.keyword.trim().toLowerCase();
return items.value.filter((item) => {
const subjectId = item?.subject_id ? String(item.subject_id) : "";
const siteId = item?.site_id || subjectSiteMap.value[subjectId] || "";
const subjectNo = String(subjectMap.value[subjectId] || "").toLowerCase();
const term = String(item?.term || "").toLowerCase();
if (keyword && !subjectNo.includes(keyword) && !term.includes(keyword)) return false;
if (filters.value.siteId && String(siteId) !== filters.value.siteId) return false;
if (filters.value.status && item?.status !== filters.value.status) return false;
if (filters.value.seriousness && item?.seriousness !== filters.value.seriousness) return false;
if (filters.value.aeType && getAeType(item) !== filters.value.aeType) return false;
return true;
});
});
const getAeType = (item: any) => {
if (item?.is_susar) return "susar";
if (item?.is_sae) return "sae";
return "ae";
};
const goSubject = (subjectId: string, tab: "ae" | "pd") => {
if (!subjectId) return;
router.push({ path: `/subjects/${subjectId}`, query: { tab } });
};
watch(
() => study.currentSite,
(newSite) => {
filters.value.siteId = newSite?.id || "";
}
);
onMounted(load);
</script>
<style scoped>
.page {
display: flex;
flex-direction: column;
}
.table-section {
padding-top: 12px;
padding-bottom: 12px;
}
.filter-form {
display: flex;
width: 100%;
flex-wrap: wrap;
gap: 0;
}
.filter-item-form {
margin-bottom: 0;
margin-right: 12px;
}
.filter-input-comp {
width: 200px;
}
.filter-select-comp {
width: 170px;
}
.filter-actions-comp {
margin-bottom: 0;
margin-right: 0;
}
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<ModulePlaceholder
:title="TEXT.modules.riskIssues.title"
:subtitle="TEXT.modules.riskIssues.subtitle"
:list-title="TEXT.modules.riskIssues.listTitle"
:empty-description="TEXT.modules.riskIssues.emptyDescription"
/>
</template>
<script setup lang="ts">
import ModulePlaceholder from "../../components/ModulePlaceholder.vue";
import { TEXT } from "../../locales";
</script>
@@ -1,7 +1,6 @@
<template>
<div class="ctms-page">
<el-card class="ctms-table-card">
<div class="page">
<el-card shadow="never" class="main-content-card unified-shell">
<el-tabs v-model="activeTab" class="ctms-tabs">
<el-tab-pane :label="TEXT.modules.startupFeasibilityEthics.feasibilityTab" name="feasibility">
<div class="ctms-tab-content">
@@ -306,14 +305,14 @@ onMounted(() => {
</script>
<style scoped>
.ctms-page {
.page {
display: flex;
flex-direction: column;
gap: 12px;
gap: 0;
}
.ctms-tab-content {
padding: 16px;
padding: 12px;
}
</style>
+5 -22
View File
@@ -1,7 +1,7 @@
<template>
<div class="page">
<el-card>
<div class="unified-shell">
<section class="unified-section">
<el-table
:data="kickoffRows"
v-loading="loading"
@@ -26,7 +26,8 @@
</el-table-column>
</el-table>
<StateEmpty v-if="!loading && kickoffRows.length === 0" :description="TEXT.modules.startupMeetingAuth.emptyKickoff" />
</el-card>
</section>
</div>
</div>
</template>
@@ -162,25 +163,7 @@ onMounted(() => {
.page {
display: flex;
flex-direction: column;
gap: 12px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
gap: 0;
}
</style>
+89 -83
View File
@@ -1,8 +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">
<el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="" class="filter-item-form">
<el-input
@@ -55,61 +54,69 @@
</el-button>
</el-form>
</div>
<el-table
:data="pagedItems"
v-loading="loading"
style="width: 100%"
class="ctms-table"
:row-class-name="subjectRowClass"
@row-click="onRowClick"
>
<el-table-column prop="subject_no" :label="TEXT.modules.subjectManagement.screeningNo" min-width="160">
<template #default="scope">
<div class="subject-info-cell">
<span class="subject-no">{{ scope.row.subject_no || TEXT.common.fallback }}</span>
<span v-if="scope.row.has_ae" class="ae-badge">AE</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">{{ siteMap[scope.row.site_id] || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="consent_date" :label="TEXT.common.fields.consentDate" width="140">
<template #default="scope">{{ displayDate(scope.row.consent_date) }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.subjectStatus, scope.row.status) }}</template>
</el-table-column>
<el-table-column prop="enrollment_date" :label="TEXT.common.fields.enrollmentDate" width="140">
<template #default="scope">{{ displayDate(scope.row.enrollment_date) }}</template>
</el-table-column>
<el-table-column prop="completion_date" :label="TEXT.common.fields.completionDate" width="140">
<template #default="scope">{{ displayDate(scope.row.completion_date) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.site_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50]"
:total="filteredItems.length"
layout="total, prev, pager, next, sizes"
/>
<div class="unified-section table-section">
<el-table
:data="pagedItems"
v-loading="loading"
style="width: 100%"
class="ctms-table"
:row-class-name="subjectRowClass"
@row-click="onRowClick"
>
<el-table-column prop="subject_no" :label="TEXT.modules.subjectManagement.screeningNo" min-width="160">
<template #default="scope">
<div class="subject-info-cell">
<span class="subject-no">{{ scope.row.subject_no || TEXT.common.fallback }}</span>
<span
v-for="badge in getAeBadges(scope.row)"
:key="badge.type"
:class="['ae-badge', `ae-badge-${badge.type}`]"
>
{{ badge.label }}
</span>
</div>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">{{ siteMap[scope.row.site_id] || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="consent_date" :label="TEXT.common.fields.consentDate" width="140">
<template #default="scope">{{ displayDate(scope.row.consent_date) }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.subjectStatus, scope.row.status) }}</template>
</el-table-column>
<el-table-column prop="enrollment_date" :label="TEXT.common.fields.enrollmentDate" width="140">
<template #default="scope">{{ displayDate(scope.row.enrollment_date) }}</template>
</el-table-column>
<el-table-column prop="completion_date" :label="TEXT.common.fields.completionDate" width="140">
<template #default="scope">{{ displayDate(scope.row.completion_date) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
type="danger"
size="small"
:disabled="isInactiveSite(scope.row.site_id)"
@click.stop="remove(scope.row)"
>
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50]"
:total="filteredItems.length"
layout="total, prev, pager, next, sizes"
/>
</div>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.subjectManagement.empty" />
</div>
<StateEmpty v-if="!loading && sortedItems.length === 0" :description="TEXT.modules.subjectManagement.empty" />
</el-card>
</div>
</template>
@@ -208,6 +215,15 @@ const remove = async (row: any) => {
};
const isInactiveSite = (siteId?: string) => !!siteId && siteActiveMap.value[siteId] === false;
const getAeBadges = (row: any) => {
const badges: Array<{ type: "ae" | "sae" | "susar"; label: string }> = [];
if (row?.has_sae) badges.push({ type: "sae", label: TEXT.modules.subjectDetail.aeType.sae });
if (row?.has_susar) badges.push({ type: "susar", label: TEXT.modules.subjectDetail.aeType.susar });
if (!row?.has_sae && !row?.has_susar && row?.has_ae) {
badges.push({ type: "ae", label: TEXT.modules.subjectDetail.aeType.ae });
}
return badges;
};
const filteredItems = computed(() => {
const keyword = filters.value.keyword.trim().toLowerCase();
return items.value.filter((item) => {
@@ -261,14 +277,6 @@ onMounted(async () => {
flex-direction: column;
}
.main-content-card :deep(.el-card__body) {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.filter-form {
display: flex;
width: 100%;
@@ -311,9 +319,8 @@ onMounted(async () => {
.ae-badge {
display: inline-flex;
align-items: center;
background: linear-gradient(135deg, #fff1f2 0%, #ffe4e6 100%);
color: #be123c;
border: 1px solid rgba(225, 29, 72, 0.2);
color: #ffffff;
border: 1px solid transparent;
padding: 2px 6px;
height: 18px;
line-height: normal;
@@ -322,7 +329,7 @@ onMounted(async () => {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.02em;
box-shadow: 0 1px 2px rgba(225, 29, 72, 0.05);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
}
.ae-badge::before {
@@ -330,29 +337,28 @@ onMounted(async () => {
display: inline-block;
width: 5px;
height: 5px;
background-color: #e11d48;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 50%;
margin-right: 4px;
}
.ae-badge-ae {
background: #3b82f6;
}
.ae-badge-sae {
background: #ef4444;
}
.ae-badge-susar {
background: #8b5cf6;
}
.table-pagination {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.table-card {
border-radius: 12px;
border: 1px solid var(--ctms-border-color);
overflow: hidden;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
</style>
<style>
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div class="page">
<div class="page-header">
<div class="page-header unified-action-bar">
<div>
<h1 class="page-title">{{ TEXT.modules.knowledgeNotes.detailTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.knowledgeNotes.detailSubtitle }}</p>
@@ -11,7 +11,7 @@
</div>
</div>
<el-card v-loading="loading">
<el-card class="unified-shell" v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.level">{{ detail.level || TEXT.common.fallback }}</el-descriptions-item>
+3 -3
View File
@@ -1,6 +1,6 @@
<template>
<div class="page">
<div class="page-header">
<div class="page-header unified-action-bar">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.knowledgeNotes.editTitle : TEXT.modules.knowledgeNotes.newTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.knowledgeNotes.formSubtitle }}</p>
@@ -8,7 +8,7 @@
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
<el-card>
<el-card class="unified-shell">
<el-form :model="form" label-width="120px">
<el-form-item :label="TEXT.common.fields.site" required>
<el-input v-model="form.site_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
@@ -36,7 +36,7 @@
:entity-id="noteId"
:readonly="isReadOnly"
/>
<el-card v-else class="hint-card">
<el-card v-else class="hint-card unified-shell">
<StateEmpty :description="TEXT.modules.knowledgeNotes.uploadHint" />
</el-card>
</div>
+41 -38
View File
@@ -1,42 +1,41 @@
<template>
<div class="ctms-page">
<div class="ctms-page-header">
<div>
<h1 class="ctms-page-title">{{ TEXT.modules.startupFeasibilityEthics.ethicsDetailTitle }}</h1>
<p class="ctms-page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.ethicsDetailSubtitle }}</p>
</div>
<div class="ctms-page-actions">
<el-button type="primary" :disabled="isInactiveSite(detail.site_id)" @click="goEdit">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
<div class="page">
<div class="unified-shell" v-loading="loading">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ TEXT.modules.startupFeasibilityEthics.ethicsDetailTitle }}</div>
<div class="ctms-section-actions">
<el-button type="primary" :disabled="isInactiveSite(detail.site_id)" @click="goEdit">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
<el-descriptions :column="2" border class="ctms-descriptions">
<el-descriptions-item :label="TEXT.common.fields.site">{{ displaySite(detail.site_id) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.approvalNo">{{ detail.approval_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.submitDate">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.acceptDate">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.meetingDate">{{ displayDate(detail.meeting_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.approvedDate">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">
<el-tag :type="statusTagType(status)">{{ statusLabel(status) }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</section>
<section class="unified-section">
<AttachmentList
v-if="recordId"
:study-id="studyId"
entity-type="startup_ethics"
:entity-id="recordId"
:readonly="isInactiveSite(detail.site_id)"
/>
</section>
</div>
<el-card class="ctms-table-card" v-loading="loading">
<el-descriptions :column="2" border class="ctms-descriptions">
<el-descriptions-item :label="TEXT.common.fields.site">{{ displaySite(detail.site_id) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.approvalNo">{{ detail.approval_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.submitDate">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.acceptDate">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.meetingDate">{{ displayDate(detail.meeting_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.approvedDate">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">
<el-tag :type="statusTagType(status)">{{ statusLabel(status) }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
<AttachmentList
v-if="recordId"
:study-id="studyId"
entity-type="startup_ethics"
:entity-id="recordId"
:readonly="isInactiveSite(detail.site_id)"
/>
</div>
</template>
@@ -158,5 +157,9 @@ onMounted(() => {
</script>
<style scoped>
/* Scoped styles can be minimized as we rely on global classes */
.page {
display: flex;
flex-direction: column;
gap: 0;
}
</style>
+104 -104
View File
@@ -1,107 +1,104 @@
<template>
<div class="ctms-page">
<div class="ctms-page-header">
<div>
<h1 class="ctms-page-title">{{ isEdit ? TEXT.modules.startupFeasibilityEthics.ethicsEditTitle : TEXT.modules.startupFeasibilityEthics.ethicsNewTitle }}</h1>
<p class="ctms-page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.ethicsFormSubtitle }}</p>
</div>
<div class="ctms-page-actions">
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
<el-card class="ctms-form-card">
<el-form :model="form" label-width="140px" class="ctms-form" label-position="top">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site">
<el-select v-model="form.site_id" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select" clearable style="width: 100%">
<el-option v-for="site in siteOptions" :key="site.value" :label="site.label" :value="site.value" :disabled="site.disabled" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.approvalNo">
<el-input v-model="form.approval_no" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.approvalNo" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="6">
<el-form-item :label="TEXT.common.fields.submitDate">
<el-date-picker
v-model="form.submit_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="TEXT.common.fields.acceptDate">
<el-date-picker
v-model="form.accept_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="TEXT.common.fields.meetingDate">
<el-date-picker
v-model="form.meeting_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="TEXT.common.fields.approvedDate">
<el-date-picker
v-model="form.approved_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="form-actions">
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="ctms-section-card" v-if="isEdit && recordId">
<div class="ctms-section-header">
<div class="ctms-section-title">{{ TEXT.common.labels.attachments }}</div>
<div class="page">
<div class="unified-shell">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ isEdit ? TEXT.modules.startupFeasibilityEthics.ethicsEditTitle : TEXT.modules.startupFeasibilityEthics.ethicsNewTitle }}</div>
<div class="ctms-section-actions">
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
<AttachmentList
:study-id="studyId"
entity-type="startup_ethics"
:entity-id="recordId"
:readonly="isReadOnly"
/>
<el-form :model="form" label-width="140px" class="ctms-form" label-position="top">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site">
<el-select v-model="form.site_id" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select" clearable style="width: 100%">
<el-option v-for="site in siteOptions" :key="site.value" :label="site.label" :value="site.value" :disabled="site.disabled" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.approvalNo">
<el-input v-model="form.approval_no" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.approvalNo" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="6">
<el-form-item :label="TEXT.common.fields.submitDate">
<el-date-picker
v-model="form.submit_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="TEXT.common.fields.acceptDate">
<el-date-picker
v-model="form.accept_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="TEXT.common.fields.meetingDate">
<el-date-picker
v-model="form.meeting_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item :label="TEXT.common.fields.approvedDate">
<el-date-picker
v-model="form.approved_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="form-actions">
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</section>
<section class="unified-section">
<template v-if="isEdit && recordId">
<div class="ctms-section-header">
<div class="ctms-section-title">{{ TEXT.common.labels.attachments }}</div>
</div>
<AttachmentList
:study-id="studyId"
entity-type="startup_ethics"
:entity-id="recordId"
:readonly="isReadOnly"
/>
</template>
<StateEmpty v-else :description="TEXT.modules.startupFeasibilityEthics.ethicsUploadHint" />
</section>
</div>
<el-card v-else class="hint-card">
<StateEmpty :description="TEXT.modules.startupFeasibilityEthics.ethicsUploadHint" />
</el-card>
</div>
</template>
@@ -221,10 +218,13 @@ onMounted(() => {
</script>
<style scoped>
.page {
display: flex;
flex-direction: column;
gap: 0;
}
.form-actions {
margin-top: 24px;
}
.hint-card {
margin-top: 16px;
}
</style>
@@ -1,41 +1,40 @@
<template>
<div class="ctms-page">
<div class="ctms-page-header">
<div>
<h1 class="ctms-page-title">{{ TEXT.modules.startupFeasibilityEthics.feasibilityDetailTitle }}</h1>
<p class="ctms-page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.feasibilityDetailSubtitle }}</p>
</div>
<div class="ctms-page-actions">
<el-button type="primary" :disabled="isInactiveSite(detail.site_id)" @click="goEdit">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
<div class="page">
<div class="unified-shell" v-loading="loading">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ TEXT.modules.startupFeasibilityEthics.feasibilityDetailTitle }}</div>
<div class="ctms-section-actions">
<el-button type="primary" :disabled="isInactiveSite(detail.site_id)" @click="goEdit">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
<el-descriptions :column="2" border class="ctms-descriptions">
<el-descriptions-item :label="TEXT.common.fields.site">{{ displaySite(detail.site_id) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.projectNo">{{ detail.project_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.submitDate">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.acceptDate">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.approvedDate">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">
<el-tag :type="statusTagType(status)">{{ statusLabel(status) }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</section>
<section class="unified-section">
<AttachmentList
v-if="recordId"
:study-id="studyId"
entity-type="startup_feasibility"
:entity-id="recordId"
:readonly="isInactiveSite(detail.site_id)"
/>
</section>
</div>
<el-card class="ctms-table-card" v-loading="loading">
<el-descriptions :column="2" border class="ctms-descriptions">
<el-descriptions-item :label="TEXT.common.fields.site">{{ displaySite(detail.site_id) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.projectNo">{{ detail.project_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.submitDate">{{ displayDate(detail.submit_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.acceptDate">{{ displayDate(detail.accept_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.approvedDate">{{ displayDate(detail.approved_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">
<el-tag :type="statusTagType(status)">{{ statusLabel(status) }}</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
<AttachmentList
v-if="recordId"
:study-id="studyId"
entity-type="startup_feasibility"
:entity-id="recordId"
:readonly="isInactiveSite(detail.site_id)"
/>
</div>
</template>
@@ -156,5 +155,9 @@ onMounted(() => {
</script>
<style scoped>
/* Scoped styles can be minimized as we rely on global classes */
.page {
display: flex;
flex-direction: column;
gap: 0;
}
</style>
+92 -92
View File
@@ -1,95 +1,92 @@
<template>
<div class="ctms-page">
<div class="ctms-page-header">
<div>
<h1 class="ctms-page-title">{{ isEdit ? TEXT.modules.startupFeasibilityEthics.feasibilityEditTitle : TEXT.modules.startupFeasibilityEthics.feasibilityNewTitle }}</h1>
<p class="ctms-page-subtitle">{{ TEXT.modules.startupFeasibilityEthics.feasibilityFormSubtitle }}</p>
</div>
<div class="ctms-page-actions">
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
<el-card class="ctms-form-card">
<el-form :model="form" label-width="140px" class="ctms-form" label-position="top">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site">
<el-select v-model="form.site_id" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select" clearable style="width: 100%">
<el-option v-for="site in siteOptions" :key="site.value" :label="site.label" :value="site.value" :disabled="site.disabled" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.projectNo">
<el-input v-model="form.project_no" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectNo" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item :label="TEXT.common.fields.submitDate">
<el-date-picker
v-model="form.submit_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="TEXT.common.fields.acceptDate">
<el-date-picker
v-model="form.accept_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="TEXT.common.fields.approvedDate">
<el-date-picker
v-model="form.approved_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="form-actions">
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="ctms-section-card" v-if="isEdit && recordId">
<div class="ctms-section-header">
<div class="ctms-section-title">{{ TEXT.common.labels.attachments }}</div>
<div class="page">
<div class="unified-shell">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ isEdit ? TEXT.modules.startupFeasibilityEthics.feasibilityEditTitle : TEXT.modules.startupFeasibilityEthics.feasibilityNewTitle }}</div>
<div class="ctms-section-actions">
<el-button @click="goBack">
<el-icon class="el-icon--left"><ArrowLeft /></el-icon>
{{ TEXT.common.actions.back }}
</el-button>
</div>
</div>
<AttachmentList
:study-id="studyId"
entity-type="startup_feasibility"
:entity-id="recordId"
:readonly="isReadOnly"
/>
<el-form :model="form" label-width="140px" class="ctms-form" label-position="top">
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site">
<el-select v-model="form.site_id" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.select" clearable style="width: 100%">
<el-option v-for="site in siteOptions" :key="site.value" :label="site.label" :value="site.value" :disabled="site.disabled" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.projectNo">
<el-input v-model="form.project_no" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.projectNo" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item :label="TEXT.common.fields.submitDate">
<el-date-picker
v-model="form.submit_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="TEXT.common.fields.acceptDate">
<el-date-picker
v-model="form.accept_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="TEXT.common.fields.approvedDate">
<el-date-picker
v-model="form.approved_date"
:disabled="isReadOnly"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="form-actions">
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</section>
<section class="unified-section">
<template v-if="isEdit && recordId">
<div class="ctms-section-header">
<div class="ctms-section-title">{{ TEXT.common.labels.attachments }}</div>
</div>
<AttachmentList
:study-id="studyId"
entity-type="startup_feasibility"
:entity-id="recordId"
:readonly="isReadOnly"
/>
</template>
<StateEmpty v-else :description="TEXT.modules.startupFeasibilityEthics.feasibilityUploadHint" />
</section>
</div>
<el-card v-else class="hint-card">
<StateEmpty :description="TEXT.modules.startupFeasibilityEthics.feasibilityUploadHint" />
</el-card>
</div>
</template>
@@ -206,10 +203,13 @@ onMounted(() => {
</script>
<style scoped>
.page {
display: flex;
flex-direction: column;
gap: 0;
}
.form-actions {
margin-top: 24px;
}
.hint-card {
margin-top: 16px;
}
</style>
+136 -160
View File
@@ -1,143 +1,143 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ TEXT.modules.startupMeetingAuth.kickoffDetailTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.kickoffDetailSubtitle }}</p>
</div>
<div class="actions">
<el-button
v-if="!editMode"
type="primary"
:disabled="loading || isReadOnly"
@click="startEdit"
<div class="unified-shell">
<section class="unified-section" v-loading="loading">
<div class="unified-section-header">
<div class="ctms-section-title">{{ TEXT.modules.startupMeetingAuth.kickoffDetailTitle }}</div>
<div class="ctms-section-actions">
<el-button
v-if="!editMode"
type="primary"
:disabled="loading || isReadOnly"
@click="startEdit"
>
{{ TEXT.common.actions.edit }}
</el-button>
<el-button v-else type="primary" :loading="saving" @click="saveEdit">{{ TEXT.common.actions.save }}</el-button>
<el-button v-if="editMode" :disabled="saving" @click="cancelEdit">{{ TEXT.common.actions.cancel }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">
{{ siteInfo.name || TEXT.common.fallback }}
</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.startupMeetingAuth.ownerLabel">
{{ ownerLabel }}
</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">
<el-tag :type="statusTagType">{{ statusLabel }}</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.kickoffDate">
<el-date-picker
v-if="editMode"
v-model="draft.kickoff_date"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
/>
<span v-else>{{ displayDate(detail.kickoff_date) }}</span>
</el-descriptions-item>
</el-descriptions>
</section>
<section class="unified-section">
<div class="training-header">
<div class="training-title">{{ TEXT.modules.startupMeetingAuth.trainingTab }}</div>
</div>
<el-table :data="trainingRows" v-loading="loadingTraining" style="width: 100%" class="ctms-table">
<el-table-column prop="name" :label="TEXT.common.fields.name" min-width="140">
<template #default="scope">
<el-input v-if="isTrainingEditing(scope.row)" v-model="trainingRowDraft.name" size="small" />
<span v-else>{{ scope.row.name || TEXT.common.fallback }}</span>
</template>
</el-table-column>
<el-table-column prop="role" :label="TEXT.common.labels.role" min-width="120">
<template #default="scope">
<el-input v-if="isTrainingEditing(scope.row)" v-model="trainingRowDraft.role" size="small" />
<span v-else>{{ scope.row.role || TEXT.common.fallback }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.trained" width="120">
<template #default="scope">
<el-checkbox
v-if="isTrainingEditing(scope.row)"
v-model="trainingRowDraft.trained"
:disabled="isReadOnly"
@click.stop
/>
<el-checkbox
v-else
v-model="scope.row.trained"
:disabled="isReadOnly"
@change="toggleTraining(scope.row)"
@click.stop
/>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.authorized" width="120">
<template #default="scope">
<el-checkbox
v-if="isTrainingEditing(scope.row)"
v-model="trainingRowDraft.authorized"
:disabled="isReadOnly"
@click.stop
/>
<el-checkbox
v-else
v-model="scope.row.authorized"
:disabled="isReadOnly"
@change="toggleTraining(scope.row)"
@click.stop
/>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<template v-if="isTrainingEditing(scope.row)">
<el-button link type="primary" size="small" :loading="savingTraining" @click.stop="saveTraining(scope.row)">
{{ TEXT.common.actions.save }}
</el-button>
<el-button link size="small" :disabled="savingTraining" @click.stop="cancelTrainingEdit">
{{ TEXT.common.actions.cancel }}
</el-button>
</template>
<template v-else>
<el-button link type="primary" size="small" :disabled="isReadOnly" @click.stop="startTrainingEdit(scope.row)">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button link type="danger" size="small" :disabled="isReadOnly" @click.stop="removeTraining(scope.row)">
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</template>
</el-table-column>
</el-table>
<div
class="training-add-row"
:class="{ disabled: newTrainingActive || savingTraining || isReadOnly }"
@click="startTrainingAdd"
>
{{ TEXT.common.actions.edit }}
</el-button>
<el-button v-else type="primary" :loading="saving" @click="saveEdit">{{ TEXT.common.actions.save }}</el-button>
<el-button v-if="editMode" :disabled="saving" @click="cancelEdit">{{ TEXT.common.actions.cancel }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<span class="training-add-icon">+</span>
</div>
<StateEmpty
v-if="!loadingTraining && trainingItems.length === 0 && !newTrainingActive"
:description="TEXT.modules.startupMeetingAuth.emptyTraining"
/>
</section>
<el-card v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">
{{ siteInfo.name || TEXT.common.fallback }}
</el-descriptions-item>
<el-descriptions-item :label="TEXT.modules.startupMeetingAuth.ownerLabel">
{{ ownerLabel }}
</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">
<el-tag :type="statusTagType">{{ statusLabel }}</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.kickoffDate">
<el-date-picker
v-if="editMode"
v-model="draft.kickoff_date"
type="date"
value-format="YYYY-MM-DD"
:placeholder="TEXT.common.placeholders.select"
<section v-if="meetingId" class="unified-section">
<div class="attachment-grid">
<AttachmentList
v-for="group in kickoffAttachmentGroups"
:key="group.entityType"
:study-id="studyId"
:entity-type="group.entityType"
:entity-id="meetingId"
:title="group.title"
/>
<span v-else>{{ displayDate(detail.kickoff_date) }}</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card>
<div class="training-header">
<div class="training-title">{{ TEXT.modules.startupMeetingAuth.trainingTab }}</div>
</div>
<el-table :data="trainingRows" v-loading="loadingTraining" style="width: 100%" class="ctms-table">
<el-table-column prop="name" :label="TEXT.common.fields.name" min-width="140">
<template #default="scope">
<el-input v-if="isTrainingEditing(scope.row)" v-model="trainingRowDraft.name" size="small" />
<span v-else>{{ scope.row.name || TEXT.common.fallback }}</span>
</template>
</el-table-column>
<el-table-column prop="role" :label="TEXT.common.labels.role" min-width="120">
<template #default="scope">
<el-input v-if="isTrainingEditing(scope.row)" v-model="trainingRowDraft.role" size="small" />
<span v-else>{{ scope.row.role || TEXT.common.fallback }}</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.trained" width="120">
<template #default="scope">
<el-checkbox
v-if="isTrainingEditing(scope.row)"
v-model="trainingRowDraft.trained"
:disabled="isReadOnly"
@click.stop
/>
<el-checkbox
v-else
v-model="scope.row.trained"
:disabled="isReadOnly"
@change="toggleTraining(scope.row)"
@click.stop
/>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.authorized" width="120">
<template #default="scope">
<el-checkbox
v-if="isTrainingEditing(scope.row)"
v-model="trainingRowDraft.authorized"
:disabled="isReadOnly"
@click.stop
/>
<el-checkbox
v-else
v-model="scope.row.authorized"
:disabled="isReadOnly"
@change="toggleTraining(scope.row)"
@click.stop
/>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<template v-if="isTrainingEditing(scope.row)">
<el-button link type="primary" size="small" :loading="savingTraining" @click.stop="saveTraining(scope.row)">
{{ TEXT.common.actions.save }}
</el-button>
<el-button link size="small" :disabled="savingTraining" @click.stop="cancelTrainingEdit">
{{ TEXT.common.actions.cancel }}
</el-button>
</template>
<template v-else>
<el-button link type="primary" size="small" :disabled="isReadOnly" @click.stop="startTrainingEdit(scope.row)">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button link type="danger" size="small" :disabled="isReadOnly" @click.stop="removeTraining(scope.row)">
{{ TEXT.common.actions.delete }}
</el-button>
</template>
</template>
</el-table-column>
</el-table>
<div
class="training-add-row"
:class="{ disabled: newTrainingActive || savingTraining || isReadOnly }"
@click="startTrainingAdd"
>
<span class="training-add-icon">+</span>
</div>
<StateEmpty
v-if="!loadingTraining && trainingItems.length === 0 && !newTrainingActive"
:description="TEXT.modules.startupMeetingAuth.emptyTraining"
/>
</el-card>
<div v-if="meetingId" class="attachment-grid">
<AttachmentList
v-for="group in kickoffAttachmentGroups"
:key="group.entityType"
:study-id="studyId"
:entity-type="group.entityType"
:entity-id="meetingId"
:title="group.title"
/>
</div>
</section>
</div>
</div>
</template>
@@ -447,35 +447,11 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.actions {
display: flex;
gap: 8px;
gap: 0;
}
.training-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
+47 -70
View File
@@ -1,54 +1,53 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupMeetingAuth.kickoffEditTitle : TEXT.modules.startupMeetingAuth.kickoffNewTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.kickoffFormSubtitle }}</p>
</div>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
<el-card>
<el-form :model="form" label-width="120px">
<el-form-item :label="TEXT.common.fields.site">
<el-input v-model="siteInfo.name" disabled />
</el-form-item>
<el-form-item :label="TEXT.modules.startupMeetingAuth.ownerLabel">
<el-input :model-value="ownerLabel" disabled />
</el-form-item>
<el-form-item :label="TEXT.common.fields.kickoffDate">
<el-date-picker v-model="form.kickoff_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.attendees">
<el-input
v-model="form.attendees"
:disabled="isReadOnly"
type="textarea"
:rows="4"
:placeholder="TEXT.modules.startupMeetingAuth.attendeesPlaceholder"
<div class="unified-shell">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ isEdit ? TEXT.modules.startupMeetingAuth.kickoffEditTitle : TEXT.modules.startupMeetingAuth.kickoffNewTitle }}</div>
<div class="ctms-section-actions">
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-form :model="form" label-width="120px">
<el-form-item :label="TEXT.common.fields.site">
<el-input v-model="siteInfo.name" disabled />
</el-form-item>
<el-form-item :label="TEXT.modules.startupMeetingAuth.ownerLabel">
<el-input :model-value="ownerLabel" disabled />
</el-form-item>
<el-form-item :label="TEXT.common.fields.kickoffDate">
<el-date-picker v-model="form.kickoff_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.attendees">
<el-input
v-model="form.attendees"
:disabled="isReadOnly"
type="textarea"
:rows="4"
:placeholder="TEXT.modules.startupMeetingAuth.attendeesPlaceholder"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</section>
<section class="unified-section">
<div v-if="isEdit && meetingId" class="attachment-grid">
<AttachmentList
v-for="group in kickoffAttachmentGroups"
:key="group.entityType"
:study-id="studyId"
:entity-type="group.entityType"
:entity-id="meetingId"
:title="group.title"
:readonly="isReadOnly"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</el-card>
<div v-if="isEdit && meetingId" class="attachment-grid">
<AttachmentList
v-for="group in kickoffAttachmentGroups"
:key="group.entityType"
:study-id="studyId"
:entity-type="group.entityType"
:entity-id="meetingId"
:title="group.title"
:readonly="isReadOnly"
/>
</div>
<StateEmpty v-else :description="TEXT.modules.startupMeetingAuth.kickoffUploadHint" />
</section>
</div>
<el-card v-else class="hint-card">
<StateEmpty :description="TEXT.modules.startupMeetingAuth.kickoffUploadHint" />
</el-card>
</div>
</template>
@@ -200,29 +199,7 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.hint-card {
padding: 12px;
gap: 0;
}
.attachment-grid {
+30 -54
View File
@@ -1,36 +1,35 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ TEXT.modules.startupMeetingAuth.trainingDetailTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.trainingDetailSubtitle }}</p>
</div>
<div class="actions">
<el-button type="primary" :disabled="isReadOnly" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
<div class="unified-shell">
<section class="unified-section" v-loading="loading">
<div class="unified-section-header">
<div class="ctms-section-title">{{ TEXT.modules.startupMeetingAuth.trainingDetailTitle }}</div>
<div class="ctms-section-actions">
<el-button type="primary" :disabled="isReadOnly" @click="goEdit">{{ TEXT.common.actions.edit }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.name">{{ detail.name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.labels.role">{{ displayEnum(TEXT.enums.userRole, detail.role) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.trained">{{ detail.trained ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.trainedDate">{{ displayDate(detail.trained_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.authorized">{{ detail.authorized ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.authorizedDate">{{ displayDate(detail.authorized_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</section>
<section class="unified-section">
<AttachmentList
v-if="recordId"
:study-id="studyId"
entity-type="training_authorization"
:entity-id="recordId"
:readonly="isReadOnly"
/>
</section>
</div>
<el-card v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.name">{{ detail.name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.labels.role">{{ displayEnum(TEXT.enums.userRole, detail.role) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.trained">{{ detail.trained ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.trainedDate">{{ displayDate(detail.trained_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.authorized">{{ detail.authorized ? TEXT.common.boolean.yes : TEXT.common.boolean.no }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.authorizedDate">{{ displayDate(detail.authorized_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</el-card>
<AttachmentList
v-if="recordId"
:study-id="studyId"
entity-type="training_authorization"
:entity-id="recordId"
:readonly="isReadOnly"
/>
</div>
</template>
@@ -115,29 +114,6 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.actions {
display: flex;
gap: 8px;
gap: 0;
}
</style>
+50 -73
View File
@@ -1,56 +1,55 @@
<template>
<div class="page">
<div class="page-header">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.startupMeetingAuth.trainingEditTitle : TEXT.modules.startupMeetingAuth.trainingNewTitle }}</h1>
<p class="page-subtitle">{{ TEXT.modules.startupMeetingAuth.trainingFormSubtitle }}</p>
</div>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
<div class="unified-shell">
<section class="unified-section">
<div class="unified-section-header">
<div class="ctms-section-title">{{ isEdit ? TEXT.modules.startupMeetingAuth.trainingEditTitle : TEXT.modules.startupMeetingAuth.trainingNewTitle }}</div>
<div class="ctms-section-actions">
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-form :model="form" label-width="120px">
<el-form-item :label="TEXT.common.fields.name" required>
<el-input v-model="form.name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
</el-form-item>
<el-form-item :label="TEXT.common.labels.role">
<el-input v-model="form.role" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.labels.role" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.site">
<el-input v-model="form.site_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.trained">
<el-switch v-model="form.trained" :disabled="isReadOnly" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.trainedDate">
<el-date-picker v-model="form.trained_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.authorized">
<el-switch v-model="form.authorized" :disabled="isReadOnly" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.authorizedDate">
<el-date-picker v-model="form.authorized_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.remark">
<el-input v-model="form.remark" :disabled="isReadOnly" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</section>
<section class="unified-section">
<AttachmentList
v-if="isEdit && recordId"
:study-id="studyId"
entity-type="training_authorization"
:entity-id="recordId"
:readonly="isReadOnly"
/>
<StateEmpty v-else :description="TEXT.modules.startupMeetingAuth.trainingUploadHint" />
</section>
</div>
<el-card>
<el-form :model="form" label-width="120px">
<el-form-item :label="TEXT.common.fields.name" required>
<el-input v-model="form.name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.name" />
</el-form-item>
<el-form-item :label="TEXT.common.labels.role">
<el-input v-model="form.role" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.labels.role" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.site">
<el-input v-model="form.site_name" :disabled="isReadOnly" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.trained">
<el-switch v-model="form.trained" :disabled="isReadOnly" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.trainedDate">
<el-date-picker v-model="form.trained_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.authorized">
<el-switch v-model="form.authorized" :disabled="isReadOnly" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.authorizedDate">
<el-date-picker v-model="form.authorized_date" :disabled="isReadOnly" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.remark">
<el-input v-model="form.remark" :disabled="isReadOnly" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" :disabled="isReadOnly" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</el-form-item>
</el-form>
</el-card>
<AttachmentList
v-if="isEdit && recordId"
:study-id="studyId"
entity-type="training_authorization"
:entity-id="recordId"
:readonly="isReadOnly"
/>
<el-card v-else class="hint-card">
<StateEmpty :description="TEXT.modules.startupMeetingAuth.trainingUploadHint" />
</el-card>
</div>
</template>
@@ -174,28 +173,6 @@ onMounted(async () => {
.page {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.page-title {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.page-subtitle {
margin: 6px 0 0;
font-size: 13px;
color: var(--ctms-text-secondary);
}
.hint-card {
padding: 12px;
gap: 0;
}
</style>
+101 -25
View File
@@ -1,7 +1,31 @@
<template>
<div class="page">
<div class="page-header unified-action-bar">
<div>
<h1 class="page-title">{{ detail.subject_no || TEXT.modules.subjectManagement.screeningNo }}</h1>
<p class="page-subtitle">{{ TEXT.modules.subjectManagement.screeningNo }}</p>
</div>
<div class="actions">
<el-button
v-if="subjectEditing"
type="primary"
:loading="subjectSaving"
:disabled="isReadOnly"
@click="saveSubjectEdit"
>
{{ TEXT.common.actions.save }}
</el-button>
<el-button v-if="subjectEditing" :disabled="isReadOnly" @click="cancelSubjectEdit">
{{ TEXT.common.actions.cancel }}
</el-button>
<el-button v-else type="primary" :disabled="isReadOnly" @click="startSubjectEdit">
{{ TEXT.common.actions.edit }}
</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
</div>
<el-card v-loading="loading">
<el-card class="unified-shell" v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.modules.subjectManagement.screeningNo">{{ detail.subject_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.site">{{ siteMap[detail.site_id] || TEXT.common.fallback }}</el-descriptions-item>
@@ -52,7 +76,7 @@
</el-descriptions>
</el-card>
<el-card>
<el-card class="unified-shell">
<el-tabs v-model="activeTab">
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.history" name="history">
<div class="tab-actions">
@@ -182,17 +206,17 @@
<el-table-column prop="onset_date" :label="TEXT.common.fields.occurDate" width="140">
<template #default="scope">{{ displayDate(scope.row.onset_date) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.fields.aeType" width="120">
<template #default="scope">
<el-tag size="small" effect="light" :class="getAeTypeClass(scope.row)">
{{ getAeTypeLabel(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="term" :label="TEXT.common.fields.title" min-width="140" />
<el-table-column prop="seriousness" :label="TEXT.common.fields.seriousness" width="120">
<template #default="scope">
<el-tag
:type="getAeSeriousnessType(normalizeSeriousness(scope.row.seriousness))"
size="small"
effect="light"
class="seriousness-tag"
>
{{ displayEnum(TEXT.enums.aeSeriousness, normalizeSeriousness(scope.row.seriousness)) }}
</el-tag>
{{ displayEnum(TEXT.enums.aeSeriousness, normalizeSeriousness(scope.row.seriousness)) }}
</template>
</el-table-column>
<el-table-column prop="causality" :label="TEXT.common.fields.causality" width="120">
@@ -227,6 +251,10 @@
</el-table>
<StateEmpty v-if="!loadingAes && aeItems.length === 0" :description="TEXT.modules.subjectDetail.emptyAe" />
</el-tab-pane>
<el-tab-pane :label="TEXT.modules.subjectDetail.tabs.pd" name="pd">
<StateEmpty :description="TEXT.modules.subjectDetail.emptyPd" />
</el-tab-pane>
</el-tabs>
</el-card>
@@ -289,6 +317,12 @@
<el-option v-for="option in aeSeriousnessOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.isSae">
<el-switch v-model="aeForm.is_sae" @change="onSaeChanged" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.isSusar">
<el-switch v-model="aeForm.is_susar" @change="onSusarChanged" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.causality">
<el-select v-model="aeForm.causality" clearable>
<el-option v-for="option in aeCausalityOptions" :key="option.value" :label="option.label" :value="option.value" />
@@ -312,7 +346,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { computed, onMounted, reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { useStudyStore } from "../../store/study";
@@ -330,6 +364,10 @@ const router = useRouter();
const study = useStudyStore();
const subjectId = route.params.subjectId as string;
const studyId = study.currentStudy?.id || "";
const resolveTabFromQuery = () => {
const tab = typeof route.query.tab === "string" ? route.query.tab : "";
return ["history", "visits", "ae", "pd"].includes(tab) ? tab : "history";
};
const loading = ref(false);
const detail = reactive<any>({
@@ -356,7 +394,7 @@ const subjectForm = reactive({
const siteMap = ref<Record<string, string>>({});
const siteActiveMap = ref<Record<string, boolean>>({});
const isReadOnly = computed(() => !!detail.site_id && siteActiveMap.value[detail.site_id] === false);
const activeTab = ref("history");
const activeTab = ref(resolveTabFromQuery());
const historyItems = ref<any[]>([]);
const loadingHistory = ref(false);
@@ -410,6 +448,8 @@ const aeForm = reactive({
term: "",
onset_date: "",
seriousness: "I",
is_sae: false,
is_susar: false,
causality: "",
outcome: "",
description: "",
@@ -714,19 +754,25 @@ const getVisitStatusTagType = (status: string) => {
}
};
const getAeSeriousnessType = (seriousness: string) => {
switch (seriousness) {
case "I":
return "info";
case "II":
return "success";
case "III":
return "warning";
case "IV":
case "V":
return "danger";
default:
return "info";
const getAeTypeKey = (row: any) => {
if (row?.is_susar) return "susar";
if (row?.is_sae) return "sae";
return "ae";
};
const getAeTypeLabel = (row: any) => TEXT.modules.subjectDetail.aeType[getAeTypeKey(row)];
const getAeTypeClass = (row: any) => `ae-type-tag ae-type-${getAeTypeKey(row)}`;
const onSaeChanged = (value: boolean) => {
if (!value) {
aeForm.is_susar = false;
}
};
const onSusarChanged = (value: boolean) => {
if (value) {
aeForm.is_sae = true;
}
};
@@ -803,6 +849,8 @@ const openAeDialog = (row?: any) => {
term: row?.term || "",
onset_date: row?.onset_date || "",
seriousness: normalizeSeriousness(row?.seriousness),
is_sae: !!row?.is_sae || !!row?.is_susar,
is_susar: !!row?.is_susar,
causality: row?.causality || "",
outcome: row?.outcome || "",
description: row?.description || "",
@@ -823,6 +871,8 @@ const saveAe = async () => {
term: aeForm.term,
onset_date: aeForm.onset_date || null,
seriousness: aeForm.seriousness,
is_sae: !!aeForm.is_sae || !!aeForm.is_susar,
is_susar: !!aeForm.is_susar,
causality: aeForm.causality || null,
outcome: aeForm.outcome || null,
description: aeForm.description || null,
@@ -857,6 +907,13 @@ const removeAe = async (row: any) => {
const goBack = () => router.push("/subjects");
watch(
() => route.query.tab,
() => {
activeTab.value = resolveTabFromQuery();
}
);
onMounted(async () => {
await loadSites();
await loadSubject();
@@ -901,4 +958,23 @@ onMounted(async () => {
justify-content: flex-end;
margin-bottom: 12px;
}
.ae-type-tag {
border: none;
}
.ae-type-ae {
color: #ffffff;
background-color: #3b82f6;
}
.ae-type-sae {
color: #ffffff;
background-color: #ef4444;
}
.ae-type-susar {
color: #ffffff;
background-color: #8b5cf6;
}
</style>
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div class="page">
<div class="page-header">
<div class="page-header unified-action-bar">
<div>
<h1 class="page-title">{{ isEdit ? TEXT.modules.subjectForm.titleEdit : TEXT.modules.subjectForm.titleNew }}</h1>
<p class="page-subtitle">{{ TEXT.modules.subjectForm.subtitle }}</p>
@@ -8,7 +8,7 @@
<el-button @click="goBack">{{ TEXT.common.actions.back }}</el-button>
</div>
<el-card>
<el-card class="unified-shell">
<el-form :model="form" label-width="120px">
<el-form-item :label="TEXT.modules.subjectManagement.screeningNo" required>
<el-input
@@ -1,5 +1,5 @@
<template>
<el-card shadow="never" class="todo-card">
<el-card shadow="never" class="todo-card unified-shell">
<template #header>
<div class="section-header">
<div class="header-left">
@@ -1,5 +1,5 @@
<template>
<el-card shadow="never" class="section-card">
<el-card shadow="never" class="section-card unified-shell">
<template #header>
<div class="section-header">
<div class="section-title-wrapper">