Step F5:受试者 & 访视管理
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
<el-menu-item index="/study/home">项目首页</el-menu-item>
|
||||
<el-menu-item index="/study/milestones">里程碑</el-menu-item>
|
||||
<el-menu-item index="/study/tasks">任务</el-menu-item>
|
||||
<el-menu-item index="/study/subjects">受试者</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<el-dialog title="新增受试者" v-model="visible" width="520px" @close="onClose">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="中心" prop="site_id">
|
||||
<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-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="受试者编号" prop="subject_no">
|
||||
<el-input v-model="form.subject_no" />
|
||||
</el-form-item>
|
||||
<el-form-item label="筛选日期" prop="screening_date">
|
||||
<el-date-picker v-model="form.screening_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="onClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { createSubject } from "../api/subjects";
|
||||
import { useStudyStore } from "../store/study";
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
sites: any[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit("update:modelValue", v),
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const study = useStudyStore();
|
||||
|
||||
const form = reactive({
|
||||
site_id: "",
|
||||
subject_no: "",
|
||||
screening_date: "",
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
site_id: [{ required: true, message: "请选择中心", trigger: "change" }],
|
||||
subject_no: [{ required: true, message: "请输入受试者编号", trigger: "blur" }],
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
if (!study.currentStudy) {
|
||||
ElMessage.error("未选择项目");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await createSubject(study.currentStudy.id, { ...form });
|
||||
ElMessage.success("提交成功");
|
||||
emit("success");
|
||||
onClose();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "提交失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<h4>访视计划</h4>
|
||||
</div>
|
||||
<el-table :data="visits" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="visit_code" label="代码" width="120" />
|
||||
<el-table-column prop="visit_name" label="名称" />
|
||||
<el-table-column prop="planned_date" label="计划日期" width="140" />
|
||||
<el-table-column prop="actual_date" label="实际日期" width="140" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column v-if="canEdit" label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link size="small" @click="markDone(scope.row)">标记完成</el-button>
|
||||
<el-button type="danger" link size="small" @click="markMissed(scope.row)">标记缺失</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { updateVisit } from "../api/visits";
|
||||
import { useStudyStore } from "../store/study";
|
||||
|
||||
interface Props {
|
||||
visits: any[];
|
||||
subjectId: string;
|
||||
canEdit: boolean;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const study = useStudyStore();
|
||||
const loading = ref(false);
|
||||
|
||||
const doUpdate = async (visitId: string, payload: Record<string, any>) => {
|
||||
if (!study.currentStudy) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await updateVisit(study.currentStudy.id, props.subjectId, visitId, payload);
|
||||
ElMessage.success("更新成功");
|
||||
props.onRefresh();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "更新失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const markDone = async (row: any) => {
|
||||
await ElMessageBox.confirm("确认标记为完成?", "提示");
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
doUpdate(row.id, { status: "DONE", actual_date: today });
|
||||
};
|
||||
|
||||
const markMissed = async (row: any) => {
|
||||
await ElMessageBox.confirm("确认标记为缺失?", "提示");
|
||||
doUpdate(row.id, { status: "MISSED", actual_date: null });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user