UI界面模拟shadcn admin效果
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="isEdit ? '编辑 AE' : '新增 AE'" v-model="visible" width="600px" @close="onClose">
|
<el-dialog :title="isEdit ? '编辑 AE' : '新增 AE'" v-model="visible" width="600px" @close="onClose" class="ctms-dialog">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="ctms-form--dense form-body">
|
||||||
<el-form-item label="受试者" prop="subject_id">
|
<el-form-item label="受试者" prop="subject_id">
|
||||||
<SubjectSelect v-model="form.subject_id" :study-id="study.currentStudy?.id || null" placeholder="可留空" />
|
<SubjectSelect v-model="form.subject_id" :study-id="study.currentStudy?.id || null" placeholder="可留空" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -26,8 +26,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="footer-right">
|
||||||
<el-button @click="onClose">取消</el-button>
|
<el-button @click="onClose">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -146,3 +148,21 @@ const onSubmit = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__footer) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="isEdit ? '编辑数据问题' : '新建数据问题'" v-model="visible" width="600px" @close="onClose">
|
<el-dialog :title="isEdit ? '编辑数据问题' : '新建数据问题'" v-model="visible" width="600px" @close="onClose" class="ctms-dialog">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="ctms-form--dense form-body">
|
||||||
<el-form-item label="标题" prop="title">
|
<el-form-item label="标题" prop="title">
|
||||||
<el-input v-model="form.title" />
|
<el-input v-model="form.title" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -33,8 +33,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="footer-right">
|
||||||
<el-button @click="onClose">取消</el-button>
|
<el-button @click="onClose">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -142,3 +144,21 @@ const onSubmit = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__footer) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="isEdit ? '编辑问题' : '新增问题'" v-model="visible" width="600px" @close="onClose">
|
<el-dialog :title="isEdit ? '编辑问题' : '新增问题'" v-model="visible" width="600px" @close="onClose" class="ctms-dialog">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="ctms-form--dense form-body">
|
||||||
<el-form-item label="标题" prop="title">
|
<el-form-item label="标题" prop="title">
|
||||||
<el-input v-model="form.title" />
|
<el-input v-model="form.title" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -25,8 +25,10 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="footer-right">
|
||||||
<el-button @click="onClose">取消</el-button>
|
<el-button @click="onClose">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -148,3 +150,21 @@ const levelLabel = (l: string) =>
|
|||||||
CRITICAL: "关键",
|
CRITICAL: "关键",
|
||||||
}[l] || l);
|
}[l] || l);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__footer) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -37,14 +37,12 @@ defineProps<Props>();
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.kpi-card {
|
.kpi-card {
|
||||||
border: 1px solid var(--ctms-border-color);
|
border: 1px solid var(--ctms-border-color);
|
||||||
transition: all 0.3s ease;
|
transition: var(--ctms-transition);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-card:hover {
|
.kpi-card:hover {
|
||||||
border-color: var(--ctms-primary-hover);
|
border-color: var(--ctms-border-color-hover);
|
||||||
box-shadow: var(--ctms-shadow-md) !important;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-header {
|
.kpi-header {
|
||||||
@@ -56,14 +54,13 @@ defineProps<Props>();
|
|||||||
|
|
||||||
.kpi-title {
|
.kpi-title {
|
||||||
color: var(--ctms-text-secondary);
|
color: var(--ctms-text-secondary);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-icon {
|
.kpi-icon {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: var(--ctms-primary);
|
color: var(--ctms-text-secondary);
|
||||||
opacity: 0.6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-content {
|
.kpi-content {
|
||||||
@@ -78,8 +75,8 @@ defineProps<Props>();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kpi-value {
|
.kpi-value {
|
||||||
font-size: 32px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--ctms-text-main);
|
color: var(--ctms-text-main);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
@@ -92,7 +89,7 @@ defineProps<Props>();
|
|||||||
|
|
||||||
.kpi-subtext {
|
.kpi-subtext {
|
||||||
color: var(--ctms-text-secondary);
|
color: var(--ctms-text-secondary);
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px dashed var(--ctms-border-color);
|
border-top: 1px dashed var(--ctms-border-color);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="layout-container">
|
<el-container class="layout-container">
|
||||||
<el-aside width="240px" class="layout-aside" v-if="isAdmin || study.currentStudy">
|
<el-aside :width="isCollapsed ? '72px' : '240px'" class="layout-aside" :class="{ collapsed: isCollapsed }" v-if="isAdmin || study.currentStudy">
|
||||||
<div class="aside-logo">
|
<div class="aside-logo">
|
||||||
<div class="logo-icon"></div>
|
<div class="logo-icon"></div>
|
||||||
<span class="logo-text">CTMS</span>
|
<span class="logo-text">CTMS</span>
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
<el-menu
|
<el-menu
|
||||||
router
|
router
|
||||||
:default-active="$route.path"
|
:default-active="$route.path"
|
||||||
|
:collapse="isCollapsed"
|
||||||
|
:collapse-transition="true"
|
||||||
class="aside-menu"
|
class="aside-menu"
|
||||||
>
|
>
|
||||||
<el-menu-item v-if="!isAdmin" index="/workbench">
|
<el-menu-item v-if="!isAdmin" index="/workbench">
|
||||||
@@ -34,32 +36,32 @@
|
|||||||
<span>项目概览</span>
|
<span>项目概览</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/study/milestones">
|
<el-menu-item index="/study/milestones">
|
||||||
<el-icon><Timer /></el-icon>
|
<el-icon><Calendar /></el-icon>
|
||||||
<span>伦理与启动</span>
|
<span>伦理与启动</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/study/subjects">
|
<el-menu-item index="/study/subjects">
|
||||||
<el-icon><UserFilled /></el-icon>
|
<el-icon><User /></el-icon>
|
||||||
<span>受试者</span>
|
<span>受试者</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/study/aes">
|
<el-menu-item index="/study/aes">
|
||||||
<el-icon><Warning /></el-icon>
|
<el-icon><WarningFilled /></el-icon>
|
||||||
<span>不良事件</span>
|
<span>不良事件</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/study/issues">
|
<el-menu-item index="/study/issues">
|
||||||
<el-icon><InfoFilled /></el-icon>
|
<el-icon><Flag /></el-icon>
|
||||||
<span>风险与问题</span>
|
<span>风险与问题</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/study/data-queries">
|
<el-menu-item index="/study/data-queries">
|
||||||
<el-icon><QuestionFilled /></el-icon>
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
<span>数据问题</span>
|
<span>数据问题</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/study/verifications">
|
<el-menu-item index="/study/verifications">
|
||||||
<el-icon><Checked /></el-icon>
|
<el-icon><CircleCheck /></el-icon>
|
||||||
<span>数据核查</span>
|
<span>数据核查</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-sub-menu index="imp">
|
<el-sub-menu index="imp">
|
||||||
<template #title>
|
<template #title>
|
||||||
<el-icon><Management /></el-icon>
|
<el-icon><Box /></el-icon>
|
||||||
<span>药品管理</span>
|
<span>药品管理</span>
|
||||||
</template>
|
</template>
|
||||||
<el-menu-item index="/study/imp/inventory">库存</el-menu-item>
|
<el-menu-item index="/study/imp/inventory">库存</el-menu-item>
|
||||||
@@ -68,15 +70,15 @@
|
|||||||
<el-menu-item index="/study/imp/transactions">交易台账</el-menu-item>
|
<el-menu-item index="/study/imp/transactions">交易台账</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
<el-menu-item index="/study/finance">
|
<el-menu-item index="/study/finance">
|
||||||
<el-icon><Money /></el-icon>
|
<el-icon><Coin /></el-icon>
|
||||||
<span>费用治理</span>
|
<span>费用治理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/study/faq">
|
<el-menu-item index="/study/faq">
|
||||||
<el-icon><Collection /></el-icon>
|
<el-icon><Notebook /></el-icon>
|
||||||
<span>知识库</span>
|
<span>知识库</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item v-if="isAdmin || isPm" index="/study/audit-logs">
|
<el-menu-item v-if="isAdmin || isPm" index="/study/audit-logs">
|
||||||
<el-icon><Memo /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
<span>审计日志</span>
|
<span>审计日志</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,7 +88,14 @@
|
|||||||
<el-container class="main-container">
|
<el-container class="main-container">
|
||||||
<el-header class="layout-header">
|
<el-header class="layout-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<!-- 移除冗余的 study-tag,由 StudySelector 统一展示交互 -->
|
<el-button class="collapse-btn" @click="toggleCollapse" aria-label="收缩侧边栏">
|
||||||
|
<el-icon class="collapse-icon">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<rect x="4" y="5" width="16" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.8" />
|
||||||
|
<line x1="9.5" y1="5" x2="9.5" y2="19" stroke="currentColor" stroke-width="1.8" />
|
||||||
|
</svg>
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<StudySelector />
|
<StudySelector />
|
||||||
@@ -133,15 +142,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useAuthStore } from "../store/auth";
|
import { useAuthStore } from "../store/auth";
|
||||||
import { useStudyStore } from "../store/study";
|
import { useStudyStore } from "../store/study";
|
||||||
import StudySelector from "./StudySelector.vue";
|
import StudySelector from "./StudySelector.vue";
|
||||||
import {
|
import {
|
||||||
Monitor, User, Suitcase, House, Timer, UserFilled,
|
Monitor, User, Suitcase, House, Calendar, WarningFilled, Flag, ChatDotRound,
|
||||||
Warning, InfoFilled, QuestionFilled, Checked, Management,
|
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton
|
||||||
Money, Collection, Memo, ArrowDown, SwitchButton
|
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
@@ -149,6 +157,12 @@ const study = useStudyStore();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
||||||
const isPm = computed(() => auth.user?.role === "PM" || study.currentStudyRole === "PM");
|
const isPm = computed(() => auth.user?.role === "PM" || study.currentStudyRole === "PM");
|
||||||
|
const isCollapsed = ref(localStorage.getItem("ctms_sidebar_collapsed") === "1");
|
||||||
|
|
||||||
|
const toggleCollapse = () => {
|
||||||
|
isCollapsed.value = !isCollapsed.value;
|
||||||
|
localStorage.setItem("ctms_sidebar_collapsed", isCollapsed.value ? "1" : "0");
|
||||||
|
};
|
||||||
|
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
auth.logout();
|
auth.logout();
|
||||||
@@ -168,16 +182,37 @@ const onCommand = (cmd: string) => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.layout-container {
|
.layout-container {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #f6f8fb;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-aside {
|
.layout-aside {
|
||||||
background-color: #001529; /* 经典深色,保持专业感 */
|
background-color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: 4px 0 10px rgba(0, 0, 0, 0.05);
|
box-shadow: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
border-right: 1px solid var(--ctms-border-color);
|
||||||
|
transition: width 0.18s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-menu {
|
||||||
|
transition: padding 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-aside.collapsed .logo-text {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-4px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-aside.collapsed .aside-logo {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-aside.collapsed .menu-divider {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aside-logo {
|
.aside-logo {
|
||||||
@@ -185,23 +220,25 @@ const onCommand = (cmd: string) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
background-color: #001529;
|
background-color: transparent;
|
||||||
|
transition: padding 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-icon {
|
.logo-icon {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
background: linear-gradient(135deg, var(--ctms-primary) 0%, #6366f1 100%);
|
background: var(--ctms-primary);
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
box-shadow: 0 4px 8px rgba(47, 84, 235, 0.3);
|
box-shadow: 0 6px 14px rgba(63, 93, 117, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-text {
|
.logo-text {
|
||||||
color: #fff;
|
color: var(--ctms-text-main);
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aside-menu {
|
.aside-menu {
|
||||||
@@ -216,7 +253,7 @@ const onCommand = (cmd: string) => {
|
|||||||
padding: 16px 24px 8px;
|
padding: 16px 24px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgba(255, 255, 255, 0.35);
|
color: var(--ctms-text-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1.5px;
|
letter-spacing: 1.5px;
|
||||||
}
|
}
|
||||||
@@ -225,47 +262,47 @@ const onCommand = (cmd: string) => {
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
line-height: 44px;
|
line-height: 44px;
|
||||||
margin: 4px 12px;
|
margin: 4px 12px;
|
||||||
border-radius: var(--ctms-radius);
|
border-radius: 12px;
|
||||||
color: rgba(255, 255, 255, 0.7) !important;
|
color: var(--ctms-text-regular) !important;
|
||||||
transition: var(--ctms-transition);
|
transition: var(--ctms-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-menu-item:hover) {
|
:deep(.el-menu-item:hover) {
|
||||||
color: #fff !important;
|
color: var(--ctms-text-main) !important;
|
||||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
background-color: var(--ctms-bg-muted) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-menu-item.is-active) {
|
:deep(.el-menu-item.is-active) {
|
||||||
color: #fff !important;
|
color: var(--ctms-primary) !important;
|
||||||
background-color: var(--ctms-primary) !important;
|
background-color: var(--ctms-primary-light) !important;
|
||||||
box-shadow: 0 4px 12px rgba(47, 84, 235, 0.2);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-menu-item.is-active::before) {
|
:deep(.el-menu-item.is-active::before) {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -12px;
|
left: 8px;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
width: 4px;
|
width: 3px;
|
||||||
background-color: #fff;
|
background-color: var(--ctms-primary);
|
||||||
border-radius: 0 4px 4px 0;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-sub-menu__title) {
|
:deep(.el-sub-menu__title) {
|
||||||
color: rgba(255, 255, 255, 0.7) !important;
|
color: var(--ctms-text-regular) !important;
|
||||||
margin: 4px 12px;
|
margin: 4px 12px;
|
||||||
border-radius: var(--ctms-radius);
|
border-radius: 12px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
line-height: 44px;
|
line-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
:deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
||||||
color: #fff !important;
|
color: var(--ctms-text-main) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-menu--inline) {
|
:deep(.el-menu--inline) {
|
||||||
background-color: rgba(0, 0, 0, 0.2) !important;
|
background-color: transparent !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,14 +312,15 @@ const onCommand = (cmd: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layout-header {
|
.layout-header {
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
|
box-shadow: none;
|
||||||
height: 64px !important;
|
height: 64px !important;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
|
border-bottom: 1px solid var(--ctms-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
@@ -291,6 +329,21 @@ const onCommand = (cmd: string) => {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn {
|
||||||
|
border: 1px solid var(--ctms-border-color);
|
||||||
|
color: var(--ctms-text-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.user-profile {
|
.user-profile {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -301,23 +354,23 @@ const onCommand = (cmd: string) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border-radius: 14px;
|
border-radius: 999px;
|
||||||
background: #f8fafc;
|
background: #ffffff;
|
||||||
border: 1px solid #eef2f6;
|
border: 1px solid var(--ctms-border-color);
|
||||||
color: #334155;
|
color: var(--ctms-text-regular);
|
||||||
transition: var(--ctms-transition);
|
transition: var(--ctms-transition);
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-trigger.compact:hover {
|
.dropdown-trigger.compact:hover {
|
||||||
background: #f1f5f9;
|
background: var(--ctms-bg-muted);
|
||||||
border-color: #e2e8f0;
|
border-color: var(--ctms-border-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar-comp {
|
.user-avatar-comp {
|
||||||
color: var(--ctms-primary) !important;
|
color: var(--ctms-primary) !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border-radius: 8px !important;
|
border-radius: 10px !important;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -339,7 +392,7 @@ const onCommand = (cmd: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
padding: 24px;
|
padding: 28px;
|
||||||
min-height: calc(100vh - 64px);
|
min-height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class="action-item"
|
class="action-item"
|
||||||
@click="go(item.path)"
|
@click="go(item.path)"
|
||||||
>
|
>
|
||||||
<div class="action-icon-wrapper" :style="{ backgroundColor: item.color + '20', color: item.color }">
|
<div class="action-icon-wrapper">
|
||||||
<el-icon><component :is="item.icon" /></el-icon>
|
<el-icon><component :is="item.icon" /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<span class="action-label">{{ item.label }}</span>
|
<span class="action-label">{{ item.label }}</span>
|
||||||
@@ -31,13 +31,13 @@ import {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
{ label: "伦理与启动", path: "/study/milestones", icon: Timer, color: "#2f54eb" },
|
{ label: "伦理与启动", path: "/study/milestones", icon: Timer },
|
||||||
{ label: "受试者", path: "/study/subjects", icon: UserFilled, color: "#722ed1" },
|
{ label: "受试者", path: "/study/subjects", icon: UserFilled },
|
||||||
{ label: "不良事件", path: "/study/aes", icon: Warning, color: "#faad14" },
|
{ label: "不良事件", path: "/study/aes", icon: Warning },
|
||||||
{ label: "数据问题", path: "/study/data-queries", icon: QuestionFilled, color: "#1890ff" },
|
{ label: "数据问题", path: "/study/data-queries", icon: QuestionFilled },
|
||||||
{ label: "药品库存", path: "/study/imp/inventory", icon: Management, color: "#52c41a" },
|
{ label: "药品库存", path: "/study/imp/inventory", icon: Management },
|
||||||
{ label: "费用治理", path: "/study/finance", icon: Money, color: "#eb2f96" },
|
{ label: "费用治理", path: "/study/finance", icon: Money },
|
||||||
{ label: "项目知识库", path: "/study/faq", icon: Collection, color: "#fa8c16" },
|
{ label: "项目知识库", path: "/study/faq", icon: Collection },
|
||||||
];
|
];
|
||||||
|
|
||||||
const go = (path: string) => router.push(path);
|
const go = (path: string) => router.push(path);
|
||||||
@@ -48,7 +48,7 @@ const go = (path: string) => router.push(path);
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: var(--ctms-radius);
|
border-radius: var(--ctms-radius);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
box-shadow: var(--ctms-shadow-sm);
|
border: 1px solid var(--ctms-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
@@ -59,12 +59,12 @@ const go = (path: string) => router.push(path);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-icon {
|
.header-icon {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
color: var(--ctms-primary);
|
color: var(--ctms-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--ctms-text-main);
|
color: var(--ctms-text-main);
|
||||||
}
|
}
|
||||||
@@ -79,31 +79,31 @@ const go = (path: string) => router.push(path);
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background-color: #fafafa;
|
background-color: #ffffff;
|
||||||
border: 1px solid var(--ctms-border-color);
|
border: 1px solid var(--ctms-border-color);
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: var(--ctms-transition);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-item:hover {
|
.action-item:hover {
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
border-color: var(--ctms-primary-hover);
|
border-color: var(--ctms-border-color-hover);
|
||||||
box-shadow: var(--ctms-shadow-sm);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-icon-wrapper {
|
.action-icon-wrapper {
|
||||||
width: 36px;
|
width: 34px;
|
||||||
height: 36px;
|
height: 34px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
color: var(--ctms-text-secondary);
|
||||||
|
background-color: var(--ctms-bg-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-label {
|
.action-label {
|
||||||
@@ -116,13 +116,11 @@ const go = (path: string) => router.push(path);
|
|||||||
.action-arrow {
|
.action-arrow {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--ctms-text-disabled);
|
color: var(--ctms-text-disabled);
|
||||||
opacity: 0;
|
opacity: 1;
|
||||||
transition: all 0.3s ease;
|
transition: var(--ctms-transition);
|
||||||
transform: translateX(-10px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-item:hover .action-arrow {
|
.action-item:hover .action-arrow {
|
||||||
opacity: 1;
|
color: var(--ctms-text-secondary);
|
||||||
transform: translateX(0);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="state state-empty">
|
||||||
|
<el-icon class="state-icon"><Document /></el-icon>
|
||||||
|
<div class="state-title">{{ title }}</div>
|
||||||
|
<div v-if="description" class="state-desc">{{ description }}</div>
|
||||||
|
<div v-if="$slots.action" class="state-action">
|
||||||
|
<slot name="action" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Document } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
title: "暂无数据",
|
||||||
|
description: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 12px;
|
||||||
|
color: var(--ctms-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
color: var(--ctms-text-disabled);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ctms-text-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ctms-text-secondary);
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="state state-error">
|
||||||
|
<el-icon class="state-icon"><CircleCloseFilled /></el-icon>
|
||||||
|
<div class="state-title">{{ title }}</div>
|
||||||
|
<div v-if="description" class="state-desc">{{ description }}</div>
|
||||||
|
<div v-if="$slots.action" class="state-action">
|
||||||
|
<slot name="action" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CircleCloseFilled } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
title: "加载失败",
|
||||||
|
description: "请稍后重试",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
color: var(--ctms-danger);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ctms-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ctms-text-secondary);
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="state state-loading">
|
||||||
|
<el-skeleton :rows="rows" animated />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
rows?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
rows: 4,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.state-loading {
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid var(--ctms-border-color);
|
||||||
|
border-radius: var(--ctms-radius);
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog title="新增受试者" v-model="visible" width="520px" @close="onClose">
|
<el-dialog title="新增受试者" v-model="visible" width="520px" @close="onClose" class="ctms-dialog">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="ctms-form--dense form-body">
|
||||||
<el-form-item label="中心" prop="site_id">
|
<el-form-item label="中心" prop="site_id">
|
||||||
<el-select v-model="form.site_id" placeholder="请选择" filterable>
|
<el-select v-model="form.site_id" placeholder="请选择" filterable>
|
||||||
<el-option v-for="s in sites" :key="s.id" :label="s.name || s.id" :value="s.id" />
|
<el-option v-for="s in sites" :key="s.id" :label="s.name || s.id" :value="s.id" />
|
||||||
@@ -14,9 +14,13 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-checkbox v-model="keepCreating" class="mr-8">提交后继续新建</el-checkbox>
|
<div class="footer-left">
|
||||||
|
<el-checkbox v-model="keepCreating">提交后继续新建</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
<el-button @click="onClose">取消</el-button>
|
<el-button @click="onClose">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -100,3 +104,25 @@ const onSubmit = async () => {
|
|||||||
|
|
||||||
resetForm();
|
resetForm();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__footer) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
color: var(--ctms-text-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
+231
-55
@@ -1,32 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* CTMS 全局样式系统
|
* CTMS 全局样式系统
|
||||||
* 基于 8px 网格系统与医疗专业色板
|
* 参考 shadcn-admin 的轻量、中性、分层风格
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* 品牌色 - 医疗蓝 */
|
/* 品牌色 - 低饱和蓝灰 */
|
||||||
--ctms-primary: #2f54eb;
|
--ctms-primary: #3f5d75;
|
||||||
--ctms-primary-hover: #597ef7;
|
--ctms-primary-hover: #365067;
|
||||||
--ctms-primary-active: #1d39c4;
|
--ctms-primary-active: #2b4356;
|
||||||
--ctms-primary-light: #f0f5ff;
|
--ctms-primary-light: #e6edf2;
|
||||||
|
|
||||||
/* 语义色 */
|
/* 语义色 */
|
||||||
--ctms-success: #52c41a;
|
--ctms-success: #3f8f6b;
|
||||||
--ctms-warning: #faad14;
|
--ctms-warning: #c58b2a;
|
||||||
--ctms-danger: #ff4d4f;
|
--ctms-danger: #c24b4b;
|
||||||
--ctms-info: #8c8c8c;
|
--ctms-info: #64748b;
|
||||||
|
|
||||||
/* 文字颜色 - 高层级到低层级 */
|
/* 文字颜色 - 高层级到低层级 */
|
||||||
--ctms-text-main: #262626;
|
--ctms-text-main: #0f172a;
|
||||||
--ctms-text-regular: #595959;
|
--ctms-text-regular: #334155;
|
||||||
--ctms-text-secondary: #8c8c8c;
|
--ctms-text-secondary: #64748b;
|
||||||
--ctms-text-disabled: #bfbfbf;
|
--ctms-text-disabled: #94a3b8;
|
||||||
|
|
||||||
/* 中性色 & 背景 */
|
/* 中性色 & 背景 */
|
||||||
--ctms-bg-base: #f0f2f5;
|
--ctms-bg-base: #f9fafb;
|
||||||
--ctms-bg-card: #ffffff;
|
--ctms-bg-card: #ffffff;
|
||||||
--ctms-border-color: #f0f0f0;
|
--ctms-bg-muted: #f3f4f6;
|
||||||
--ctms-border-color-hover: #d9d9d9;
|
--ctms-border-color: #e5e7eb;
|
||||||
|
--ctms-border-color-hover: #cbd5e1;
|
||||||
|
|
||||||
/* 间距 - 8px 网格 */
|
/* 间距 - 8px 网格 */
|
||||||
--ctms-spacing-xs: 4px;
|
--ctms-spacing-xs: 4px;
|
||||||
@@ -35,19 +38,31 @@
|
|||||||
--ctms-spacing-lg: 24px;
|
--ctms-spacing-lg: 24px;
|
||||||
--ctms-spacing-xl: 32px;
|
--ctms-spacing-xl: 32px;
|
||||||
|
|
||||||
/* 圆角 & 阴影 - 升级为更多层级 */
|
/* 圆角 & 阴影 */
|
||||||
--ctms-radius-sm: 4px;
|
--ctms-radius-sm: 6px;
|
||||||
--ctms-radius: 8px;
|
--ctms-radius: 10px;
|
||||||
--ctms-radius-lg: 12px;
|
--ctms-radius-lg: 12px;
|
||||||
--ctms-radius-round: 50%;
|
--ctms-radius-round: 50%;
|
||||||
|
|
||||||
--ctms-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.04);
|
--ctms-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
--ctms-shadow: 0 8px 24px rgba(149, 157, 165, 0.1);
|
--ctms-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
|
||||||
--ctms-shadow-md: 0 12px 32px rgba(0, 0, 0, 0.08);
|
--ctms-shadow-md: 0 12px 26px rgba(15, 23, 42, 0.08);
|
||||||
--ctms-shadow-lg: 0 20px 40px rgba(0, 0, 0, 0.12);
|
--ctms-shadow-lg: 0 20px 40px rgba(15, 23, 42, 0.1);
|
||||||
|
|
||||||
/* 动画 */
|
/* 动画 */
|
||||||
--ctms-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
--ctms-transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
|
||||||
|
/* Element Plus 主色适配 */
|
||||||
|
--el-color-primary: var(--ctms-primary);
|
||||||
|
--el-color-primary-light-3: #9bb1c2;
|
||||||
|
--el-color-primary-light-5: #c0cdd7;
|
||||||
|
--el-color-primary-light-7: #dde5ec;
|
||||||
|
--el-color-primary-light-8: #eef2f6;
|
||||||
|
--el-color-primary-dark-2: #2b4356;
|
||||||
|
--el-border-radius-base: var(--ctms-radius);
|
||||||
|
--el-border-color: var(--ctms-border-color);
|
||||||
|
--el-text-color-primary: var(--ctms-text-main);
|
||||||
|
--el-text-color-regular: var(--ctms-text-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 现代滚动条 */
|
/* 现代滚动条 */
|
||||||
@@ -72,8 +87,8 @@
|
|||||||
/* 全局重置 */
|
/* 全局重置 */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: "Manrope", "Segoe UI", sans-serif;
|
||||||
background-color: var(--ctms-bg-base);
|
background: var(--ctms-bg-base);
|
||||||
color: var(--ctms-text-main);
|
color: var(--ctms-text-main);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
@@ -83,8 +98,8 @@ body {
|
|||||||
/* 按钮美化 */
|
/* 按钮美化 */
|
||||||
.el-button {
|
.el-button {
|
||||||
border-radius: var(--ctms-radius);
|
border-radius: var(--ctms-radius);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
transition: var(--ctms-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button--primary {
|
.el-button--primary {
|
||||||
@@ -97,11 +112,50 @@ body {
|
|||||||
border-color: var(--ctms-primary-hover);
|
border-color: var(--ctms-primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-button--default {
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: var(--ctms-border-color);
|
||||||
|
color: var(--ctms-text-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--default:hover {
|
||||||
|
border-color: var(--ctms-border-color-hover);
|
||||||
|
color: var(--ctms-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger.is-plain {
|
||||||
|
color: var(--ctms-danger);
|
||||||
|
border-color: rgba(194, 75, 75, 0.4);
|
||||||
|
background-color: rgba(194, 75, 75, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--default,
|
||||||
|
.el-button--primary,
|
||||||
|
.el-button--danger,
|
||||||
|
.el-button--success,
|
||||||
|
.el-button--warning {
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--small {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--large {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 卡片美化 */
|
/* 卡片美化 */
|
||||||
.el-card {
|
.el-card {
|
||||||
border: none;
|
border: 1px solid var(--ctms-border-color);
|
||||||
box-shadow: var(--ctms-shadow-sm) !important;
|
box-shadow: none !important;
|
||||||
border-radius: var(--ctms-radius);
|
border-radius: var(--ctms-radius-lg);
|
||||||
margin-bottom: var(--ctms-spacing-md);
|
margin-bottom: var(--ctms-spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,18 +176,18 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-table th.el-table__cell {
|
.el-table th.el-table__cell {
|
||||||
background-color: #fafafa;
|
background-color: #f6f7f9;
|
||||||
color: var(--ctms-text-main);
|
color: var(--ctms-text-main);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
height: 48px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table td.el-table__cell {
|
.el-table td.el-table__cell {
|
||||||
padding: 12px 0; /* 增加行高 */
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-table--enable-row-hover .el-table__row:hover > td.el-table__cell {
|
.el-table--enable-row-hover .el-table__row:hover > td.el-table__cell {
|
||||||
background-color: var(--ctms-primary-light);
|
background-color: #f2f5f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单美化 */
|
/* 表单美化 */
|
||||||
@@ -146,40 +200,162 @@ body {
|
|||||||
border-radius: var(--ctms-radius);
|
border-radius: var(--ctms-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper {
|
||||||
|
border-radius: var(--ctms-radius);
|
||||||
|
background-color: #ffffff;
|
||||||
|
min-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select .el-input__wrapper {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input--small .el-input__wrapper,
|
||||||
|
.el-select--small .el-input__wrapper {
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input--large .el-input__wrapper,
|
||||||
|
.el-select--large .el-input__wrapper {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-form--dense .el-form-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__error {
|
||||||
|
color: #b45309;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 标签美化 */
|
/* 标签美化 */
|
||||||
.el-tag {
|
.el-tag {
|
||||||
border-radius: 2px;
|
border-radius: 999px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tag--info { background-color: #f5f5f5; color: var(--ctms-text-secondary); }
|
.el-tag--info { background-color: #f5f5f5; color: var(--ctms-text-secondary); }
|
||||||
.el-tag--success { background-color: #f6ffed; color: var(--ctms-success); border: 1px solid #b7eb8f; }
|
.el-tag--success { background-color: rgba(63, 143, 107, 0.12); color: var(--ctms-success); border: 1px solid rgba(63, 143, 107, 0.2); }
|
||||||
.el-tag--warning { background-color: #fffbe6; color: var(--ctms-warning); border: 1px solid #ffe58f; }
|
.el-tag--warning { background-color: rgba(197, 139, 42, 0.12); color: var(--ctms-warning); border: 1px solid rgba(197, 139, 42, 0.2); }
|
||||||
.el-tag--danger { background-color: #fff1f0; color: var(--ctms-danger); border: 1px solid #ffa39e; }
|
.el-tag--danger { background-color: rgba(194, 75, 75, 0.12); color: var(--ctms-danger); border: 1px solid rgba(194, 75, 75, 0.2); }
|
||||||
|
|
||||||
/* 列表整合工具栏 (Table Toolbar) */
|
.el-alert {
|
||||||
.ctms-table-toolbar {
|
border: 1px solid var(--ctms-border-color);
|
||||||
display: flex;
|
background-color: #ffffff;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctms-filter-group {
|
.el-alert__title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ctms-text-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-alert--warning .el-alert__icon {
|
||||||
|
color: var(--ctms-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-alert--success .el-alert__icon {
|
||||||
|
color: var(--ctms-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-alert--error .el-alert__icon {
|
||||||
|
color: var(--ctms-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button.is-disabled,
|
||||||
|
.el-input.is-disabled .el-input__wrapper,
|
||||||
|
.el-select.is-disabled .el-input__wrapper {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-empty__description {
|
||||||
|
color: var(--ctms-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-skeleton__item {
|
||||||
|
background: linear-gradient(90deg, #f2f3f5 25%, #eceef1 37%, #f2f3f5 63%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面结构与筛选区域 */
|
||||||
|
.ctms-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--ctms-spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-page-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ctms-text-main);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ctms-page-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-filter-card {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 20px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctms-filter-item {
|
.ctms-filter-item {
|
||||||
width: 160px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctms-action-group {
|
.ctms-filter-label {
|
||||||
display: flex;
|
font-size: 12px;
|
||||||
gap: 12px;
|
font-weight: 600;
|
||||||
align-items: center;
|
color: var(--ctms-text-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-filter-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-table-card {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-table .el-table__row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-section-card {
|
||||||
|
border: 1px solid var(--ctms-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctms-section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ctms-text-main);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" v-if="ae">
|
<div class="ctms-page" v-if="ae">
|
||||||
<el-card class="mb-12">
|
<div class="ctms-page-header">
|
||||||
<div class="header">
|
|
||||||
<div>
|
<div>
|
||||||
<h3>AE(不良事件)</h3>
|
<h1 class="ctms-page-title">不良事件</h1>
|
||||||
<p>{{ ae.term }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="ctms-page-actions">
|
||||||
<template v-for="action in availableActions" :key="action.key">
|
<template v-for="action in availableActions" :key="action.key">
|
||||||
<PermissionAction action="ae.close">
|
<PermissionAction action="ae.close">
|
||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
:type="action.danger ? 'danger' : 'primary'"
|
:type="action.danger ? 'danger' : 'primary'"
|
||||||
|
:link="action.danger"
|
||||||
@click="onAction(action)"
|
@click="onAction(action)"
|
||||||
>
|
>
|
||||||
{{ action.label }}
|
{{ action.label }}
|
||||||
@@ -20,19 +19,32 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-descriptions :column="2" border>
|
|
||||||
|
<el-card class="ctms-section-card">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">基本信息</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-descriptions :column="2" border class="detail-descriptions">
|
||||||
<el-descriptions-item label="受试者">{{ subjectName(ae.subject_id) }}</el-descriptions-item>
|
<el-descriptions-item label="受试者">{{ subjectName(ae.subject_id) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="严重性">{{ ae.seriousness }}</el-descriptions-item>
|
<el-descriptions-item label="严重性">{{ ae.seriousness }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="严重程度">{{ ae.severity }}</el-descriptions-item>
|
<el-descriptions-item label="严重程度">{{ ae.severity }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="发生日期">{{ ae.onset_date }}</el-descriptions-item>
|
<el-descriptions-item label="发生日期">{{ ae.onset_date }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="stateColor(aeState)">{{ stateLabel(aeState) }}</el-tag>
|
<el-tag :type="stateColor(aeState)" effect="plain">{{ stateLabel(aeState) }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="描述">{{ ae.description }}</el-descriptions-item>
|
<el-descriptions-item label="描述">{{ ae.description }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-tabs>
|
<el-card class="ctms-section-card">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">协作记录</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-tabs class="detail-tabs">
|
||||||
<el-tab-pane label="评论">
|
<el-tab-pane label="评论">
|
||||||
<CommentList :study-id="studyId" entity-type="aes" :entity-id="ae.id" :can-comment="true" />
|
<CommentList :study-id="studyId" entity-type="aes" :entity-id="ae.id" :can-comment="true" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -45,9 +57,10 @@
|
|||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="page">
|
<div v-else class="ctms-page">
|
||||||
<el-skeleton rows="4" animated />
|
<StateLoading :rows="4" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -62,6 +75,7 @@ import { useAuthStore } from "../store/auth";
|
|||||||
import CommentList from "../components/CommentList.vue";
|
import CommentList from "../components/CommentList.vue";
|
||||||
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
||||||
import PermissionAction from "../components/PermissionAction.vue";
|
import PermissionAction from "../components/PermissionAction.vue";
|
||||||
|
import StateLoading from "../components/StateLoading.vue";
|
||||||
import { usePermission } from "../utils/permission";
|
import { usePermission } from "../utils/permission";
|
||||||
import { aeMachine, getAvailableActions } from "../state-machine";
|
import { aeMachine, getAvailableActions } from "../state-machine";
|
||||||
import type { ActionConfig } from "../state-machine";
|
import type { ActionConfig } from "../state-machine";
|
||||||
@@ -199,15 +213,11 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.detail-descriptions :deep(.el-descriptions__label) {
|
||||||
padding: 16px;
|
color: var(--ctms-text-secondary);
|
||||||
}
|
}
|
||||||
.header {
|
|
||||||
display: flex;
|
.detail-tabs {
|
||||||
align-items: center;
|
margin-top: -8px;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.mb-12 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+30
-30
@@ -1,21 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="ctms-page">
|
||||||
<el-card class="mb-12">
|
<div class="ctms-page-header">
|
||||||
<div class="filters">
|
<div>
|
||||||
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadAes">
|
<h1 class="ctms-page-title">不良事件</h1>
|
||||||
|
</div>
|
||||||
|
<div class="ctms-page-actions">
|
||||||
|
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建不良事件</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card class="ctms-filter-card">
|
||||||
|
<div class="ctms-filter-row">
|
||||||
|
<div class="ctms-filter-item">
|
||||||
|
<span class="ctms-filter-label">状态</span>
|
||||||
|
<el-select v-model="filters.status" placeholder="全部" clearable @change="loadAes">
|
||||||
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
|
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select v-model="filters.seriousness" placeholder="严重性" clearable @change="loadAes">
|
</div>
|
||||||
|
<div class="ctms-filter-item">
|
||||||
|
<span class="ctms-filter-label">严重性</span>
|
||||||
|
<el-select v-model="filters.seriousness" placeholder="全部" clearable @change="loadAes">
|
||||||
<el-option v-for="s in seriousnessOptions" :key="s" :label="seriousnessLabel(s)" :value="s" />
|
<el-option v-for="s in seriousnessOptions" :key="s" :label="seriousnessLabel(s)" :value="s" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-switch v-model="filters.overdue" active-text="仅逾期" @change="loadAes" />
|
</div>
|
||||||
<div class="spacer" />
|
<div class="ctms-filter-item">
|
||||||
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建不良事件</el-button>
|
<span class="ctms-filter-label">仅逾期</span>
|
||||||
|
<el-switch v-model="filters.overdue" @change="loadAes" />
|
||||||
|
</div>
|
||||||
|
<div class="ctms-filter-spacer" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card>
|
<el-card class="ctms-table-card">
|
||||||
<el-table :data="aes" v-loading="loading" style="width: 100%" @row-click="goDetail">
|
<el-table :data="aes" v-loading="loading" class="ctms-table" style="width: 100%" @row-click="goDetail">
|
||||||
<el-table-column prop="term" label="事件" />
|
<el-table-column prop="term" label="事件" />
|
||||||
<el-table-column label="受试者" width="200">
|
<el-table-column label="受试者" width="200">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -34,12 +51,13 @@
|
|||||||
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ statusLabel(scope.row.status) }}</el-tag>
|
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="140">
|
<el-table-column label="操作" width="120" align="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="showClose(scope.row)"
|
v-if="showClose(scope.row)"
|
||||||
type="danger"
|
type="danger"
|
||||||
size="small"
|
size="small"
|
||||||
|
link
|
||||||
@click.stop="onClose(scope.row)"
|
@click.stop="onClose(scope.row)"
|
||||||
>
|
>
|
||||||
关闭 AE
|
关闭 AE
|
||||||
@@ -48,7 +66,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
class="pagination"
|
class="ctms-pagination"
|
||||||
layout="prev, pager, next"
|
layout="prev, pager, next"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:current-page="page"
|
:current-page="page"
|
||||||
@@ -244,22 +262,4 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.pagination {
|
|
||||||
margin-top: 12px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.mb-12 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="ctms-page">
|
||||||
<el-card class="mb-12">
|
<div class="ctms-page-header">
|
||||||
<div class="filters">
|
<div>
|
||||||
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadQueries">
|
<h1 class="ctms-page-title">数据问题</h1>
|
||||||
|
</div>
|
||||||
|
<div class="ctms-page-actions">
|
||||||
|
<el-button type="primary" v-if="canEdit" @click="showForm = true">新建数据问题</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card class="ctms-filter-card">
|
||||||
|
<div class="ctms-filter-row">
|
||||||
|
<div class="ctms-filter-item">
|
||||||
|
<span class="ctms-filter-label">状态</span>
|
||||||
|
<el-select v-model="filters.status" placeholder="全部" clearable @change="loadQueries">
|
||||||
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
|
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-switch v-model="filters.overdue" active-text="仅逾期" @change="loadQueries" />
|
</div>
|
||||||
<el-input v-model="filters.assigned_to" placeholder="负责人" style="width: 180px" @change="loadQueries" />
|
<div class="ctms-filter-item">
|
||||||
<div class="spacer" />
|
<span class="ctms-filter-label">负责人</span>
|
||||||
<el-button type="primary" v-if="canEdit" @click="showForm = true">新建数据问题</el-button>
|
<el-input v-model="filters.assigned_to" placeholder="输入邮箱或姓名" @change="loadQueries" />
|
||||||
|
</div>
|
||||||
|
<div class="ctms-filter-item">
|
||||||
|
<span class="ctms-filter-label">仅逾期</span>
|
||||||
|
<el-switch v-model="filters.overdue" @change="loadQueries" />
|
||||||
|
</div>
|
||||||
|
<div class="ctms-filter-spacer" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card>
|
<el-card class="ctms-table-card">
|
||||||
<el-table :data="queries" v-loading="loading" style="width: 100%" @row-click="goDetail">
|
<el-table :data="queries" v-loading="loading" class="ctms-table" style="width: 100%" @row-click="goDetail">
|
||||||
<el-table-column prop="title" label="标题" />
|
<el-table-column prop="title" label="标题" />
|
||||||
<el-table-column label="受试者">
|
<el-table-column label="受试者">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -36,7 +53,7 @@
|
|||||||
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ statusLabel(scope.row.status) }}</el-tag>
|
<el-tag :type="scope.row.is_overdue ? 'danger' : 'info'">{{ statusLabel(scope.row.status) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="140">
|
<el-table-column label="操作" width="120" align="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="showResolve(scope.row)"
|
v-if="showResolve(scope.row)"
|
||||||
@@ -50,7 +67,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
class="pagination"
|
class="ctms-pagination"
|
||||||
layout="prev, pager, next"
|
layout="prev, pager, next"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:current-page="page"
|
:current-page="page"
|
||||||
@@ -264,22 +281,4 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.pagination {
|
|
||||||
margin-top: 12px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.mb-12 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" v-if="query">
|
<div class="ctms-page" v-if="query">
|
||||||
<el-card class="mb-12">
|
<div class="ctms-page-header">
|
||||||
<div class="header">
|
|
||||||
<div>
|
<div>
|
||||||
<h3>数据问题详情</h3>
|
<h1 class="ctms-page-title">数据问题</h1>
|
||||||
<p>{{ query.title }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ctms-page-actions">
|
||||||
<el-button v-if="canEdit" type="primary" size="small" @click="nextStatus">流转状态</el-button>
|
<el-button v-if="canEdit" type="primary" size="small" @click="nextStatus">流转状态</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-descriptions :column="2" border>
|
</div>
|
||||||
|
|
||||||
|
<el-card class="ctms-section-card">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">基础信息</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-descriptions :column="2" border class="detail-descriptions">
|
||||||
<el-descriptions-item label="受试者">
|
<el-descriptions-item label="受试者">
|
||||||
{{ subjectMap[query.subject_id] || query.subject_id || "-" }}
|
{{ subjectMap[query.subject_id] || query.subject_id || "-" }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -16,13 +23,19 @@
|
|||||||
<el-descriptions-item label="优先级">{{ query.priority }}</el-descriptions-item>
|
<el-descriptions-item label="优先级">{{ query.priority }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="截止日期">{{ query.due_date }}</el-descriptions-item>
|
<el-descriptions-item label="截止日期">{{ query.due_date }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="query.is_overdue ? 'danger' : 'info'">{{ query.status }}</el-tag>
|
<el-tag :type="query.is_overdue ? 'danger' : 'info'" effect="plain">{{ query.status }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="描述">{{ query.description }}</el-descriptions-item>
|
<el-descriptions-item label="描述">{{ query.description }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-tabs>
|
<el-card class="ctms-section-card">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">协作记录</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-tabs class="detail-tabs">
|
||||||
<el-tab-pane label="评论">
|
<el-tab-pane label="评论">
|
||||||
<CommentList :study-id="studyId" entity-type="data-queries" :entity-id="query.id" :can-comment="true" />
|
<CommentList :study-id="studyId" entity-type="data-queries" :entity-id="query.id" :can-comment="true" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -35,9 +48,10 @@
|
|||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="page">
|
<div v-else class="ctms-page">
|
||||||
<el-skeleton rows="4" animated />
|
<StateLoading :rows="4" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -52,6 +66,7 @@ import { useAuthStore } from "../store/auth";
|
|||||||
import CommentList from "../components/CommentList.vue";
|
import CommentList from "../components/CommentList.vue";
|
||||||
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
||||||
import { usePermission } from "../utils/permission";
|
import { usePermission } from "../utils/permission";
|
||||||
|
import StateLoading from "../components/StateLoading.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const study = useStudyStore();
|
const study = useStudyStore();
|
||||||
@@ -122,15 +137,11 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.detail-descriptions :deep(.el-descriptions__label) {
|
||||||
padding: 16px;
|
color: var(--ctms-text-secondary);
|
||||||
}
|
}
|
||||||
.header {
|
|
||||||
display: flex;
|
.detail-tabs {
|
||||||
align-items: center;
|
margin-top: -8px;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.mb-12 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" v-if="issue">
|
<div class="ctms-page" v-if="issue">
|
||||||
<el-card class="mb-12">
|
<div class="ctms-page-header">
|
||||||
<div class="header">
|
|
||||||
<div>
|
<div>
|
||||||
<h3>风险与问题</h3>
|
<h1 class="ctms-page-title">风险与问题</h1>
|
||||||
<p>{{ issue.title }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ctms-page-actions">
|
||||||
<PermissionAction action="issue.close">
|
<PermissionAction action="issue.close">
|
||||||
<el-button type="danger" size="small" @click="closeIssue">关闭</el-button>
|
<el-button type="danger" size="small" link @click="closeIssue">关闭</el-button>
|
||||||
</PermissionAction>
|
</PermissionAction>
|
||||||
</div>
|
</div>
|
||||||
<el-descriptions :column="2" border>
|
</div>
|
||||||
|
|
||||||
|
<el-card class="ctms-section-card">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">基础信息</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-descriptions :column="2" border class="detail-descriptions">
|
||||||
<el-descriptions-item label="类别">{{ issue.category }}</el-descriptions-item>
|
<el-descriptions-item label="类别">{{ issue.category }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="等级">{{ issue.level }}</el-descriptions-item>
|
<el-descriptions-item label="等级">{{ issue.level }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="截止日期">{{ issue.due_date }}</el-descriptions-item>
|
<el-descriptions-item label="截止日期">{{ issue.due_date }}</el-descriptions-item>
|
||||||
@@ -19,7 +26,13 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-tabs>
|
<el-card class="ctms-section-card">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">协作记录</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-tabs class="detail-tabs">
|
||||||
<el-tab-pane label="评论">
|
<el-tab-pane label="评论">
|
||||||
<CommentList :study-id="studyId" entity-type="issues" :entity-id="issue.id" :can-comment="true" />
|
<CommentList :study-id="studyId" entity-type="issues" :entity-id="issue.id" :can-comment="true" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -32,9 +45,10 @@
|
|||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="page">
|
<div v-else class="ctms-page">
|
||||||
<el-skeleton rows="4" animated />
|
<StateLoading :rows="4" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -49,6 +63,7 @@ import PermissionAction from "../components/PermissionAction.vue";
|
|||||||
import { usePermission } from "../utils/permission";
|
import { usePermission } from "../utils/permission";
|
||||||
import CommentList from "../components/CommentList.vue";
|
import CommentList from "../components/CommentList.vue";
|
||||||
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
||||||
|
import StateLoading from "../components/StateLoading.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const study = useStudyStore();
|
const study = useStudyStore();
|
||||||
@@ -97,15 +112,11 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.detail-descriptions :deep(.el-descriptions__label) {
|
||||||
padding: 16px;
|
color: var(--ctms-text-secondary);
|
||||||
}
|
}
|
||||||
.header {
|
|
||||||
display: flex;
|
.detail-tabs {
|
||||||
align-items: center;
|
margin-top: -8px;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.mb-12 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="ctms-page">
|
||||||
<el-card class="mb-12">
|
<div class="ctms-page-header">
|
||||||
<div class="filters">
|
<div>
|
||||||
<el-select v-model="filters.status" placeholder="状态" clearable @change="loadIssues">
|
<h1 class="ctms-page-title">风险与问题</h1>
|
||||||
|
</div>
|
||||||
|
<div class="ctms-page-actions">
|
||||||
|
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建风险/问题</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card class="ctms-filter-card">
|
||||||
|
<div class="ctms-filter-row">
|
||||||
|
<div class="ctms-filter-item">
|
||||||
|
<span class="ctms-filter-label">状态</span>
|
||||||
|
<el-select v-model="filters.status" placeholder="全部" clearable @change="loadIssues">
|
||||||
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
|
<el-option v-for="s in statuses" :key="s" :label="statusLabel(s)" :value="s" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select v-model="filters.level" placeholder="等级" clearable @change="loadIssues">
|
</div>
|
||||||
|
<div class="ctms-filter-item">
|
||||||
|
<span class="ctms-filter-label">等级</span>
|
||||||
|
<el-select v-model="filters.level" placeholder="全部" clearable @change="loadIssues">
|
||||||
<el-option v-for="l in levels" :key="l" :label="levelLabel(l)" :value="l" />
|
<el-option v-for="l in levels" :key="l" :label="levelLabel(l)" :value="l" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select v-model="filters.category" placeholder="类别" clearable @change="loadIssues">
|
</div>
|
||||||
|
<div class="ctms-filter-item">
|
||||||
|
<span class="ctms-filter-label">类别</span>
|
||||||
|
<el-select v-model="filters.category" placeholder="全部" clearable @change="loadIssues">
|
||||||
<el-option v-for="c in categories" :key="c" :label="categoryLabel(c)" :value="c" />
|
<el-option v-for="c in categories" :key="c" :label="categoryLabel(c)" :value="c" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-switch v-model="filters.overdue" active-text="仅逾期" @change="loadIssues" />
|
</div>
|
||||||
<div class="spacer" />
|
<div class="ctms-filter-item">
|
||||||
<el-button type="primary" v-if="canCreate" @click="showForm = true">新建风险/问题</el-button>
|
<span class="ctms-filter-label">仅逾期</span>
|
||||||
|
<el-switch v-model="filters.overdue" @change="loadIssues" />
|
||||||
|
</div>
|
||||||
|
<div class="ctms-filter-spacer" />
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card>
|
<el-card class="ctms-table-card">
|
||||||
<el-table :data="issues" v-loading="loading" style="width: 100%" @row-click="goDetail">
|
<el-table :data="issues" v-loading="loading" class="ctms-table" style="width: 100%" @row-click="goDetail">
|
||||||
<el-table-column prop="title" label="标题" />
|
<el-table-column prop="title" label="标题" />
|
||||||
<el-table-column prop="level" label="等级" width="120">
|
<el-table-column prop="level" label="等级" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
@@ -38,7 +58,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-pagination
|
<el-pagination
|
||||||
class="pagination"
|
class="ctms-pagination"
|
||||||
layout="prev, pager, next"
|
layout="prev, pager, next"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:current-page="page"
|
:current-page="page"
|
||||||
@@ -159,22 +179,4 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.pagination {
|
|
||||||
margin-top: 12px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.mb-12 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<!-- 动态背景元素 -->
|
|
||||||
<div class="bg-animate">
|
|
||||||
<div class="circle circle-1"></div>
|
|
||||||
<div class="circle circle-2"></div>
|
|
||||||
<div class="circle circle-3"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="login-content">
|
<div class="login-content">
|
||||||
<div class="login-brand">
|
<div class="login-brand">
|
||||||
<h1 class="brand-title">CTMS</h1>
|
<h1 class="brand-title">CTMS</h1>
|
||||||
@@ -145,62 +138,11 @@ const onSubmit = async () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f8fafc;
|
background-color: var(--ctms-bg-base);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 动态背景 */
|
|
||||||
.bg-animate {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 0;
|
|
||||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: blur(80px);
|
|
||||||
z-index: 0;
|
|
||||||
opacity: 0.4;
|
|
||||||
animation: move 20s infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle-1 {
|
|
||||||
width: 500px;
|
|
||||||
height: 500px;
|
|
||||||
background: #3b82f6;
|
|
||||||
top: -100px;
|
|
||||||
left: -100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle-2 {
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
background: #10b981;
|
|
||||||
bottom: -50px;
|
|
||||||
right: -50px;
|
|
||||||
animation-delay: -5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle-3 {
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
background: #6366f1;
|
|
||||||
top: 40%;
|
|
||||||
left: 30%;
|
|
||||||
animation-delay: -10s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes move {
|
|
||||||
from { transform: translate(0, 0) scale(1); }
|
|
||||||
to { transform: translate(50px, 100px) scale(1.1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-content {
|
.login-content {
|
||||||
width: 420px;
|
width: 420px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -214,17 +156,15 @@ const onSubmit = async () => {
|
|||||||
|
|
||||||
.brand-title {
|
.brand-title {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
background: linear-gradient(135deg, var(--ctms-primary) 0%, #6366f1 100%);
|
color: var(--ctms-text-main);
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
letter-spacing: -1px;
|
letter-spacing: -1px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-subtitle {
|
.brand-subtitle {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #64748b;
|
color: var(--ctms-text-secondary);
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -232,11 +172,10 @@ const onSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
border: 1px solid var(--ctms-border-color);
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: #ffffff;
|
||||||
backdrop-filter: blur(20px);
|
border-radius: 16px;
|
||||||
border-radius: 20px;
|
box-shadow: none;
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,34 +187,34 @@ const onSubmit = async () => {
|
|||||||
.form-title {
|
.form-title {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1e293b;
|
color: var(--ctms-text-main);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-desc {
|
.form-desc {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #94a3b8;
|
color: var(--ctms-text-secondary);
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.elegant-form :deep(.el-form-item__label) {
|
.elegant-form :deep(.el-form-item__label) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #475569;
|
color: var(--ctms-text-regular);
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.elegant-form :deep(.el-input__wrapper) {
|
.elegant-form :deep(.el-input__wrapper) {
|
||||||
background-color: #f8fafc;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 0 0 1px #e2e8f0 inset;
|
box-shadow: 0 0 0 1px var(--ctms-border-color) inset;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: var(--ctms-transition);
|
transition: var(--ctms-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.elegant-form :deep(.el-input__wrapper.is-focus) {
|
.elegant-form :deep(.el-input__wrapper.is-focus) {
|
||||||
box-shadow: 0 0 0 1px var(--ctms-primary) inset, 0 0 0 4px rgba(47, 84, 235, 0.1) !important;
|
box-shadow: 0 0 0 1px var(--ctms-primary) inset !important;
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-options {
|
.form-options {
|
||||||
@@ -298,22 +237,21 @@ const onSubmit = async () => {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: linear-gradient(135deg, var(--ctms-primary) 0%, #4f46e5 100%);
|
background: var(--ctms-primary);
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
box-shadow: none;
|
||||||
transition: var(--ctms-transition);
|
transition: var(--ctms-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn:hover {
|
.submit-btn:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.register-prompt {
|
.register-prompt {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #64748b;
|
color: var(--ctms-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.register-prompt a {
|
.register-prompt a {
|
||||||
@@ -330,6 +268,6 @@ const onSubmit = async () => {
|
|||||||
|
|
||||||
.login-footer p {
|
.login-footer p {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #94a3b8;
|
color: var(--ctms-text-disabled);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" v-if="milestone">
|
<div class="ctms-page" v-if="milestone">
|
||||||
<el-card class="mb-12">
|
<div class="ctms-page-header">
|
||||||
<div class="header">
|
|
||||||
<div>
|
<div>
|
||||||
<h3>节点详情</h3>
|
<h1 class="ctms-page-title">节点详情</h1>
|
||||||
<p class="subtitle">{{ milestone.name }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-descriptions :column="2" border>
|
|
||||||
|
<el-card class="ctms-section-card">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">基础信息</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-descriptions :column="2" border class="detail-descriptions">
|
||||||
<el-descriptions-item label="名称">{{ milestone.name || "—" }}</el-descriptions-item>
|
<el-descriptions-item label="名称">{{ milestone.name || "—" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="类型">{{ typeLabel(milestone.type) }}</el-descriptions-item>
|
<el-descriptions-item label="类型">{{ typeLabel(milestone.type) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="中心">{{ siteName(milestone) }}</el-descriptions-item>
|
<el-descriptions-item label="中心">{{ siteName(milestone) }}</el-descriptions-item>
|
||||||
@@ -15,13 +20,19 @@
|
|||||||
<el-descriptions-item label="计划日期">{{ displayDate(milestone.planned_date) }}</el-descriptions-item>
|
<el-descriptions-item label="计划日期">{{ displayDate(milestone.planned_date) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="实际日期">{{ displayDate(milestone.actual_date) }}</el-descriptions-item>
|
<el-descriptions-item label="实际日期">{{ displayDate(milestone.actual_date) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="当前状态">
|
<el-descriptions-item label="当前状态">
|
||||||
<el-tag :type="statusColor(milestone.status)">{{ statusLabel(milestone.status) }}</el-tag>
|
<el-tag :type="statusColor(milestone.status)" effect="plain">{{ statusLabel(milestone.status) }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注" :span="2">{{ milestone.notes || "—" }}</el-descriptions-item>
|
<el-descriptions-item label="备注" :span="2">{{ milestone.notes || "—" }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-tabs>
|
<el-card class="ctms-section-card">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">协作记录</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-tabs class="detail-tabs">
|
||||||
<el-tab-pane label="评论">
|
<el-tab-pane label="评论">
|
||||||
<CommentList
|
<CommentList
|
||||||
v-if="milestone.id"
|
v-if="milestone.id"
|
||||||
@@ -44,9 +55,10 @@
|
|||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="page">
|
<div v-else class="ctms-page">
|
||||||
<el-skeleton rows="4" animated />
|
<StateLoading :rows="4" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -61,6 +73,7 @@ import { useStudyStore } from "../store/study";
|
|||||||
import { useAuthStore } from "../store/auth";
|
import { useAuthStore } from "../store/auth";
|
||||||
import CommentList from "../components/CommentList.vue";
|
import CommentList from "../components/CommentList.vue";
|
||||||
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
import AttachmentList from "../components/attachments/AttachmentList.vue";
|
||||||
|
import StateLoading from "../components/StateLoading.vue";
|
||||||
import {
|
import {
|
||||||
getMilestoneStatusColor,
|
getMilestoneStatusColor,
|
||||||
getMilestoneStatusLabel,
|
getMilestoneStatusLabel,
|
||||||
@@ -143,28 +156,22 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.detail-descriptions :deep(.el-descriptions__label) {
|
||||||
padding: 16px;
|
color: var(--ctms-text-secondary);
|
||||||
}
|
}
|
||||||
.header {
|
|
||||||
display: flex;
|
.detail-tabs {
|
||||||
align-items: center;
|
margin-top: -8px;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
margin: 4px 0 0;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.mb-12 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip {
|
.tip {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: #666;
|
color: var(--ctms-text-regular);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-tip {
|
.sub-tip {
|
||||||
color: #888;
|
color: var(--ctms-text-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<div class="home-header">
|
<div class="home-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1 class="welcome-title">项目概览</h1>
|
<h1 class="welcome-title">项目概览</h1>
|
||||||
|
<p class="page-subtitle">当前项目运行状态与关键指标</p>
|
||||||
<p class="study-info">
|
<p class="study-info">
|
||||||
<span class="study-name">{{ study.currentStudy.name }}</span>
|
<span class="study-name">{{ study.currentStudy.name }}</span>
|
||||||
<span class="study-divider">/</span>
|
<span class="study-divider">/</span>
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-meta">
|
<div class="header-meta">
|
||||||
<el-tag effect="plain" class="role-tag">我的角色: {{ auth.user?.role }}</el-tag>
|
<el-tag effect="plain" class="role-tag">角色: {{ auth.user?.role }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="empty-state-container">
|
<div v-else class="empty-state-container">
|
||||||
<el-empty description="请先在顶部选择一个当前项目" />
|
<StateEmpty description="请先在顶部选择一个当前项目" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ import { fetchFinanceSummary, fetchOverdueAesCount, fetchOverdueQueriesCount, fe
|
|||||||
import KpiCard from "../components/KpiCard.vue";
|
import KpiCard from "../components/KpiCard.vue";
|
||||||
import QuickActions from "../components/QuickActions.vue";
|
import QuickActions from "../components/QuickActions.vue";
|
||||||
import { List, Warning, QuestionFilled, Money } from "@element-plus/icons-vue";
|
import { List, Warning, QuestionFilled, Money } from "@element-plus/icons-vue";
|
||||||
|
import StateEmpty from "../components/StateEmpty.vue";
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const study = useStudyStore();
|
const study = useStudyStore();
|
||||||
@@ -181,23 +183,29 @@ watch(
|
|||||||
.home-header {
|
.home-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-title {
|
.welcome-title {
|
||||||
font-size: 28px;
|
font-size: 26px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--ctms-text-main);
|
color: var(--ctms-text-main);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ctms-text-secondary);
|
||||||
|
margin: 6px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.study-info {
|
.study-info {
|
||||||
margin: 8px 0 0;
|
margin: 6px 0 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: var(--ctms-text-secondary);
|
color: var(--ctms-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,22 +214,21 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.role-tag {
|
.role-tag {
|
||||||
background-color: #fff;
|
background-color: #ffffff;
|
||||||
border-color: var(--ctms-primary-hover);
|
border-color: var(--ctms-border-color);
|
||||||
color: var(--ctms-primary);
|
color: var(--ctms-text-regular);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 20px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-alert {
|
.home-alert {
|
||||||
border: none;
|
border: 1px solid var(--ctms-border-color);
|
||||||
border-left: 4px solid;
|
border-radius: var(--ctms-radius);
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-nav-section {
|
.quick-nav-section {
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
<div class="brand-info">
|
<div class="brand-info">
|
||||||
<h1 class="subject-no">{{ subject.subject_no }}</h1>
|
<h1 class="subject-no">{{ subject.subject_no }}</h1>
|
||||||
<p class="site-info">{{ siteName(subject.site_id) }}</p>
|
<p class="site-info">{{ siteName(subject.site_id) }}</p>
|
||||||
|
<p class="meta-info">受试者详情与状态管理</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="subject-status">
|
<div class="subject-status">
|
||||||
<span class="status-label">当前状态</span>
|
<span class="status-label">当前状态</span>
|
||||||
<el-tag :type="stateColor(subject.status)" effect="dark" class="status-tag">
|
<el-tag :type="stateColor(subject.status)" effect="plain" class="status-tag">
|
||||||
{{ stateLabel(subject.status) }}
|
{{ stateLabel(subject.status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,8 +23,9 @@
|
|||||||
<template v-for="action in availableActions" :key="action.key">
|
<template v-for="action in availableActions" :key="action.key">
|
||||||
<PermissionAction :action="permissionMap[action.key]">
|
<PermissionAction :action="permissionMap[action.key]">
|
||||||
<el-button
|
<el-button
|
||||||
:type="action.danger ? 'danger' : 'primary'"
|
:type="action.danger ? 'danger' : (action.primary ? 'primary' : 'default')"
|
||||||
:plain="!action.primary"
|
:plain="!action.primary && !action.danger"
|
||||||
|
:link="action.danger"
|
||||||
size="default"
|
size="default"
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
@click="onAction(action)"
|
@click="onAction(action)"
|
||||||
@@ -42,6 +44,7 @@
|
|||||||
<el-icon><Calendar /></el-icon>
|
<el-icon><Calendar /></el-icon>
|
||||||
<span>访视记录</span>
|
<span>访视记录</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section-desc">按访视节点维护受试者随访数据</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<VisitTable :visits="visits" :subject-id="subject.id" :can-edit="canVisitEdit" :on-refresh="loadVisits" />
|
<VisitTable :visits="visits" :subject-id="subject.id" :can-edit="canVisitEdit" :on-refresh="loadVisits" />
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="loading-state">
|
<div v-else class="loading-state">
|
||||||
<el-skeleton :rows="5" animated />
|
<StateLoading :rows="5" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -67,6 +70,7 @@ import { useAuthStore } from "../store/auth";
|
|||||||
import PermissionAction from "../components/PermissionAction.vue";
|
import PermissionAction from "../components/PermissionAction.vue";
|
||||||
import { usePermission } from "../utils/permission";
|
import { usePermission } from "../utils/permission";
|
||||||
import VisitTable from "../components/VisitTable.vue";
|
import VisitTable from "../components/VisitTable.vue";
|
||||||
|
import StateLoading from "../components/StateLoading.vue";
|
||||||
import { getAvailableActions, subjectMachine } from "../state-machine";
|
import { getAvailableActions, subjectMachine } from "../state-machine";
|
||||||
import type { ActionConfig } from "../state-machine";
|
import type { ActionConfig } from "../state-machine";
|
||||||
import { statusDict, getDictLabel, getDictColor } from "../dictionaries";
|
import { statusDict, getDictLabel, getDictColor } from "../dictionaries";
|
||||||
@@ -214,7 +218,7 @@ onMounted(async () => {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: var(--ctms-radius);
|
border-radius: var(--ctms-radius);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
box-shadow: var(--ctms-shadow-sm);
|
border: 1px solid var(--ctms-border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -232,31 +236,37 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand-avatar {
|
.brand-avatar {
|
||||||
width: 56px;
|
width: 48px;
|
||||||
height: 56px;
|
height: 48px;
|
||||||
background-color: var(--ctms-primary-light);
|
background-color: #f1f5f9;
|
||||||
color: var(--ctms-primary);
|
color: var(--ctms-text-main);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject-no {
|
.subject-no {
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--ctms-text-main);
|
color: var(--ctms-text-main);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-info {
|
.site-info {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: var(--ctms-text-secondary);
|
color: var(--ctms-text-secondary);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meta-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ctms-text-disabled);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.subject-status {
|
.subject-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -264,7 +274,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-label {
|
.status-label {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--ctms-text-secondary);
|
color: var(--ctms-text-secondary);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -287,7 +297,7 @@ onMounted(async () => {
|
|||||||
.info-section {
|
.info-section {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: var(--ctms-radius);
|
border-radius: var(--ctms-radius);
|
||||||
box-shadow: var(--ctms-shadow-sm);
|
border: 1px solid var(--ctms-border-color);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +313,13 @@ onMounted(async () => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--ctms-text-main);
|
color: var(--ctms-text-main);
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ctms-text-secondary);
|
||||||
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-body {
|
.section-body {
|
||||||
@@ -314,5 +330,6 @@ onMounted(async () => {
|
|||||||
padding: 40px;
|
padding: 40px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: var(--ctms-radius);
|
border-radius: var(--ctms-radius);
|
||||||
|
border: 1px solid var(--ctms-border-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -68,17 +68,14 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="screening_date" label="筛选日期" width="140" />
|
<el-table-column prop="screening_date" label="筛选日期" width="140" />
|
||||||
<el-table-column prop="enrollment_date" label="入组日期" width="140" />
|
<el-table-column prop="enrollment_date" label="入组日期" width="140" />
|
||||||
<el-table-column label="操作" width="100" fixed="right">
|
<el-table-column label="操作" width="100" fixed="right" align="right">
|
||||||
<template #default>
|
<template #default>
|
||||||
<el-button link type="primary">详细资料</el-button>
|
<el-button link type="primary">详细资料</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="empty-state">
|
<StateEmpty description="暂无符合条件的受试者" />
|
||||||
<el-icon class="empty-icon"><Document /></el-icon>
|
|
||||||
<p>暂无符合条件的受试者</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
@@ -102,12 +99,13 @@
|
|||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { Plus, Document } from "@element-plus/icons-vue";
|
import { Plus } from "@element-plus/icons-vue";
|
||||||
import { fetchSubjects } from "../api/subjects";
|
import { fetchSubjects } from "../api/subjects";
|
||||||
import { fetchSites } from "../api/sites";
|
import { fetchSites } from "../api/sites";
|
||||||
import { useStudyStore } from "../store/study";
|
import { useStudyStore } from "../store/study";
|
||||||
import { useAuthStore } from "../store/auth";
|
import { useAuthStore } from "../store/auth";
|
||||||
import SubjectForm from "../components/SubjectForm.vue";
|
import SubjectForm from "../components/SubjectForm.vue";
|
||||||
|
import StateEmpty from "../components/StateEmpty.vue";
|
||||||
|
|
||||||
const study = useStudyStore();
|
const study = useStudyStore();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
@@ -228,18 +226,18 @@ const siteName = (id: string) => siteMap.value[id] || id || "-";
|
|||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--ctms-text-main);
|
color: var(--ctms-text-main);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-subtitle {
|
.page-subtitle {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: var(--ctms-text-secondary);
|
color: var(--ctms-text-secondary);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
@@ -252,11 +250,12 @@ const siteName = (id: string) => siteMap.value[id] || id || "-";
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-items {
|
.filter-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-item {
|
.filter-item {
|
||||||
@@ -266,8 +265,8 @@ const siteName = (id: string) => siteMap.value[id] || id || "-";
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: var(--ctms-text-regular);
|
color: var(--ctms-text-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,25 +280,15 @@ const siteName = (id: string) => siteMap.value[id] || id || "-";
|
|||||||
|
|
||||||
.subject-no {
|
.subject-no {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--ctms-primary);
|
color: var(--ctms-text-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
.pagination-container {
|
||||||
margin-top: 24px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
padding: 40px 0;
|
|
||||||
color: var(--ctms-text-disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__row) {
|
:deep(.el-table__row) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="ctms-page">
|
||||||
<el-card class="mb-12">
|
<div class="ctms-page-header">
|
||||||
<div class="header">
|
|
||||||
<div>
|
<div>
|
||||||
<h3>项目详情</h3>
|
<h1 class="ctms-page-title">项目详情</h1>
|
||||||
<p class="sub" v-if="project">项目:{{ project.code }} - {{ project.name }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="ctms-page-actions">
|
||||||
<el-button type="primary" @click="enterProject" :disabled="!project">进入项目</el-button>
|
<el-button type="primary" @click="enterProject" :disabled="!project">进入项目</el-button>
|
||||||
<el-button @click="goMembers" :disabled="!project">项目成员配置</el-button>
|
<el-button @click="goMembers" :disabled="!project">项目成员配置</el-button>
|
||||||
<el-button @click="goSites" :disabled="!project">中心管理</el-button>
|
<el-button @click="goSites" :disabled="!project">中心管理</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="mb-12">
|
<el-card class="ctms-section-card">
|
||||||
<h4 class="section-title">项目基本信息</h4>
|
<template #header>
|
||||||
<el-descriptions v-if="project" :column="2" border>
|
<div>
|
||||||
|
<div class="ctms-section-title">项目基本信息</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-descriptions v-if="project" :column="2" border class="detail-descriptions">
|
||||||
<el-descriptions-item label="项目编号">{{ project.code }}</el-descriptions-item>
|
<el-descriptions-item label="项目编号">{{ project.code }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目名称">{{ project.name }}</el-descriptions-item>
|
<el-descriptions-item label="项目名称">{{ project.name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="申办方">{{ project.sponsor || "—" }}</el-descriptions-item>
|
<el-descriptions-item label="申办方">{{ project.sponsor || "—" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">{{ statusLabel(project.status) }}</el-descriptions-item>
|
<el-descriptions-item label="状态">{{ statusLabel(project.status) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ displayDateTime(project.created_at) }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ displayDateTime(project.created_at) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
<el-skeleton v-else :rows="4" animated />
|
<StateLoading v-else :rows="4" />
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card>
|
<el-card class="ctms-section-card">
|
||||||
<h4 class="section-title">项目通用文件</h4>
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<div class="ctms-section-title">项目通用文件</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
v-if="project"
|
v-if="project"
|
||||||
:study-id="project.id"
|
:study-id="project.id"
|
||||||
@@ -35,7 +40,7 @@
|
|||||||
:entity-id="project.id"
|
:entity-id="project.id"
|
||||||
:show-uploader="true"
|
:show-uploader="true"
|
||||||
/>
|
/>
|
||||||
<el-skeleton v-else :rows="3" animated />
|
<StateLoading v-else :rows="3" />
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -47,6 +52,7 @@ import { ElMessage } from "element-plus";
|
|||||||
import { fetchStudyDetail } from "../../api/studies";
|
import { fetchStudyDetail } from "../../api/studies";
|
||||||
import type { Study } from "../../types/api";
|
import type { Study } from "../../types/api";
|
||||||
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
import AttachmentList from "../../components/attachments/AttachmentList.vue";
|
||||||
|
import StateLoading from "../../components/StateLoading.vue";
|
||||||
import { useStudyStore } from "../../store/study";
|
import { useStudyStore } from "../../store/study";
|
||||||
import { displayDateTime } from "../../utils/display";
|
import { displayDateTime } from "../../utils/display";
|
||||||
|
|
||||||
@@ -94,26 +100,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.detail-descriptions :deep(.el-descriptions__label) {
|
||||||
padding: 16px;
|
color: var(--ctms-text-secondary);
|
||||||
}
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.sub {
|
|
||||||
color: #666;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
.section-title {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.mb-12 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty-state">
|
||||||
<el-empty description="暂无待办事项,记录一些备忘吧" :image-size="60" />
|
<StateEmpty description="暂无待办事项,记录一些备忘吧" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import { Notebook, EditPen, Plus, Delete } from "@element-plus/icons-vue";
|
import { Notebook, EditPen, Plus, Delete } from "@element-plus/icons-vue";
|
||||||
|
import StateEmpty from "../../../components/StateEmpty.vue";
|
||||||
|
|
||||||
const props = defineProps<{ storageKey: string | undefined }>();
|
const props = defineProps<{ storageKey: string | undefined }>();
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<el-skeleton :loading="loading" animated :rows="3">
|
<StateLoading v-if="loading" :rows="3" />
|
||||||
<template #default>
|
<div v-else-if="items && items.length" class="list-container">
|
||||||
<div v-if="items && items.length" class="list-container">
|
|
||||||
<div
|
<div
|
||||||
v-for="item in items.slice(0, 5)"
|
v-for="item in items.slice(0, 5)"
|
||||||
:key="item.title + item.path"
|
:key="item.title + item.path"
|
||||||
@@ -36,10 +35,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty-state">
|
||||||
<el-empty :description="emptyText" :image-size="60" />
|
<StateEmpty :description="emptyText || '暂无数据'" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</el-skeleton>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
@@ -47,6 +44,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ArrowRight } from "@element-plus/icons-vue";
|
import { ArrowRight } from "@element-plus/icons-vue";
|
||||||
|
import StateEmpty from "../../../components/StateEmpty.vue";
|
||||||
|
import StateLoading from "../../../components/StateLoading.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
@@ -147,11 +146,6 @@ const go = (path: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 40px 0;
|
padding: 8px 0;
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-empty__description) {
|
|
||||||
margin-top: 8px;
|
|
||||||
color: var(--ctms-text-disabled);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user