药品流向管理内容优化-初步

This commit is contained in:
Cheng Zhou
2026-01-13 19:42:28 +08:00
parent 1db36f40b0
commit ef1e67218c
21 changed files with 344 additions and 480 deletions
+47 -17
View File
@@ -11,20 +11,38 @@
</div>
</div>
<el-card v-loading="loading">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.direction">{{ displayEnum(TEXT.enums.shipmentDirection, detail.direction) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.status">{{ displayEnum(TEXT.enums.shipmentStatus, detail.status) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.shipDate">{{ displayDate(detail.ship_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.carrier">{{ detail.carrier || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.trackingNo">{{ detail.tracking_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.fromParty">{{ detail.from_party || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.toParty">{{ detail.to_party || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.drugDesc" :span="2">{{ detail.drug_desc || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</el-card>
<div class="content">
<el-card>
<template #header>
<div class="card-header">
<span>{{ TEXT.common.labels.basicInfo }}</span>
<el-tag :type="detail.status === 'PENDING' ? 'info' : detail.status === 'SIGNED' ? 'success' : 'primary'">
{{ displayEnum(TEXT.enums.shipmentStatus, detail.status) }}
</el-tag>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.site">{{ detail.site_name || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.direction">
<el-tag :type="detail.direction === 'SEND' ? 'primary' : 'warning'" size="small">
{{ displayEnum(TEXT.enums.shipmentDirection, detail.direction) }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card :header="TEXT.modules.drugShipments.recordLabel">
<el-descriptions :column="2" border>
<el-descriptions-item :label="TEXT.common.fields.trackingNo">{{ detail.tracking_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.carrier">{{ detail.carrier || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.shipDate">{{ displayDate(detail.ship_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.receiveDate">{{ displayDate(detail.receive_date) }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.quantity">{{ detail.quantity ?? TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.batchNo">{{ detail.batch_no || TEXT.common.fallback }}</el-descriptions-item>
<el-descriptions-item :label="TEXT.common.fields.remark" :span="2">{{ detail.remark || TEXT.common.fallback }}</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<AttachmentList
v-if="shipmentId"
@@ -57,11 +75,11 @@ const detail = reactive<any>({
status: "",
site_name: "",
ship_date: "",
receive_date: "",
quantity: null,
batch_no: "",
carrier: "",
tracking_no: "",
from_party: "",
to_party: "",
drug_desc: "",
remark: "",
});
@@ -109,6 +127,18 @@ onMounted(load);
color: var(--ctms-text-secondary);
}
.content {
display: flex;
flex-direction: column;
gap: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.actions {
display: flex;
gap: 8px;
+135 -53
View File
@@ -9,49 +9,85 @@
</div>
<el-card>
<el-form :model="form" label-width="120px">
<el-form-item :label="TEXT.common.fields.direction" required>
<el-select v-model="form.direction" :placeholder="TEXT.common.placeholders.select">
<el-option :label="TEXT.enums.shipmentDirection.SEND" value="SEND" />
<el-option :label="TEXT.enums.shipmentDirection.RETURN" value="RETURN" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.site" required>
<el-input v-model="form.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.drugDesc" required>
<el-input v-model="form.drug_desc" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.shipDate">
<el-date-picker v-model="form.ship_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.carrier">
<el-input v-model="form.carrier" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.carrier" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.trackingNo">
<el-input v-model="form.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.fromParty">
<el-input v-model="form.from_party" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.fromParty" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.toParty">
<el-input v-model="form.to_party" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.toParty" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.status" required>
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select">
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
<el-option :label="TEXT.enums.shipmentStatus.RETURNED" value="RETURNED" />
<el-option :label="TEXT.enums.shipmentStatus.EXCEPTION" value="EXCEPTION" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.remark">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="top">
<h3 class="section-title">{{ TEXT.common.labels.basicInfo }}</h3>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.site" prop="center_id">
<el-select v-model="form.center_id" :placeholder="TEXT.common.placeholders.select" style="width: 100%">
<el-option
v-for="site in sites"
:key="site.id"
:label="site.name || TEXT.common.fallback"
:value="site.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.direction" prop="direction">
<el-select v-model="form.direction" :placeholder="TEXT.common.placeholders.select" style="width: 100%">
<el-option :label="TEXT.enums.shipmentDirection.SEND" value="SEND" />
<el-option :label="TEXT.enums.shipmentDirection.RETURN" value="RETURN" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.status" prop="status">
<el-select v-model="form.status" :placeholder="TEXT.common.placeholders.select" style="width: 100%">
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
<el-option :label="TEXT.enums.shipmentStatus.RETURNED" value="RETURNED" />
<el-option :label="TEXT.enums.shipmentStatus.EXCEPTION" value="EXCEPTION" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<h3 class="section-title">{{ TEXT.modules.drugShipments.recordLabel }}</h3>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.shipDate" prop="ship_date">
<el-date-picker v-model="form.ship_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.receiveDate" prop="receive_date">
<el-date-picker v-model="form.receive_date" type="date" value-format="YYYY-MM-DD" :placeholder="TEXT.common.placeholders.select" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.quantity" prop="quantity">
<el-input-number v-model="form.quantity" :min="0" :controls="false" :placeholder="TEXT.common.placeholders.input" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.batchNo" prop="batch_no">
<el-input v-model="form.batch_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.batchNo" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.carrier" prop="carrier">
<el-input v-model="form.carrier" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.carrier" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="TEXT.common.fields.trackingNo" prop="tracking_no">
<el-input v-model="form.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" />
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="TEXT.common.fields.remark" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="TEXT.common.placeholders.input" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
<div class="actions">
<el-button type="primary" :loading="saving" @click="submit">{{ TEXT.common.actions.save }}</el-button>
<el-button @click="goBack">{{ TEXT.common.actions.cancel }}</el-button>
</div>
</el-form-item>
</el-form>
</el-card>
@@ -74,6 +110,7 @@ import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { useStudyStore } from "../../store/study";
import { createDrugShipment, getDrugShipment, updateDrugShipment } from "../../api/drugShipments";
import { fetchSites } from "../../api/sites";
import AttachmentList from "../../components/attachments/AttachmentList.vue";
import StateEmpty from "../../components/StateEmpty.vue";
import { TEXT } from "../../locales";
@@ -82,37 +119,63 @@ const route = useRoute();
const router = useRouter();
const study = useStudyStore();
const saving = ref(false);
const formRef = ref();
const sites = ref<any[]>([]);
const shipmentId = computed(() => route.params.shipmentId as string | undefined);
const isEdit = computed(() => !!shipmentId.value);
const studyId = computed(() => study.currentStudy?.id || "");
const form = reactive({
center_id: "",
direction: "SEND",
site_name: "",
drug_desc: "",
ship_date: "",
receive_date: "",
quantity: null as number | null,
batch_no: "",
carrier: "",
tracking_no: "",
from_party: "",
to_party: "",
status: "PENDING",
remark: "",
});
const rules = {
center_id: [{ required: true, message: TEXT.common.messages.required, trigger: "change" }],
direction: [{ required: true, message: TEXT.common.messages.required, trigger: "change" }],
ship_date: [{ required: true, message: TEXT.common.messages.required, trigger: "change" }],
receive_date: [{ required: true, message: TEXT.common.messages.required, trigger: "change" }],
quantity: [{ required: true, type: "number", message: TEXT.common.messages.required, trigger: "change" }],
batch_no: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
carrier: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
tracking_no: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
status: [{ required: true, message: TEXT.common.messages.required, trigger: "change" }],
remark: [{ required: true, message: TEXT.common.messages.required, trigger: "blur" }],
};
const loadSites = async () => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
try {
const { data } = await fetchSites(studyId, { limit: 500 });
sites.value = Array.isArray(data) ? data : data.items || [];
} catch {
sites.value = [];
}
};
const load = async () => {
if (!isEdit.value || !studyId.value || !shipmentId.value) return;
try {
const { data } = await getDrugShipment(studyId.value, shipmentId.value);
Object.assign(form, {
direction: data.direction || "SEND",
site_name: data.site_name || "",
drug_desc: data.drug_desc || "",
center_id: data.center_id || "",
ship_date: data.ship_date || "",
receive_date: data.receive_date || "",
quantity: data.quantity ?? null,
batch_no: data.batch_no || "",
carrier: data.carrier || "",
tracking_no: data.tracking_no || "",
from_party: data.from_party || "",
to_party: data.to_party || "",
status: data.status || "PENDING",
remark: data.remark || "",
});
@@ -123,17 +186,19 @@ const load = async () => {
const submit = async () => {
if (!studyId.value) return;
const valid = await formRef.value?.validate().catch(() => false);
if (!valid) return;
saving.value = true;
try {
const payload = {
center_id: form.center_id,
direction: form.direction,
site_name: form.site_name,
drug_desc: form.drug_desc,
ship_date: form.ship_date || null,
receive_date: form.receive_date || null,
quantity: form.quantity,
batch_no: form.batch_no,
carrier: form.carrier || null,
tracking_no: form.tracking_no || null,
from_party: form.from_party || null,
to_party: form.to_party || null,
status: form.status,
remark: form.remark || null,
};
@@ -155,7 +220,10 @@ const submit = async () => {
const goBack = () => router.push("/drug/shipments");
onMounted(load);
onMounted(async () => {
await loadSites();
load();
});
</script>
<style scoped>
@@ -186,4 +254,18 @@ onMounted(load);
.hint-card {
padding: 12px;
}
.section-title {
margin: 0 0 24px;
font-size: 16px;
font-weight: 600;
color: var(--ctms-text-primary);
border-left: 4px solid var(--ctms-primary);
padding-left: 12px;
}
.actions {
display: flex;
gap: 12px;
}
</style>
+1 -1
View File
@@ -157,7 +157,7 @@
{{ scope.row.remark || TEXT.common.fallback }}
</template>
</el-table-column>
<el-table-column :label="TEXT.common.actions.delete" width="120">
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
+1 -1
View File
@@ -155,7 +155,7 @@
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column :label="TEXT.common.actions.delete" width="120">
<el-table-column :label="TEXT.common.labels.actions" width="120">
<template #default="scope">
<el-button
link
+88 -23
View File
@@ -11,13 +11,23 @@
<el-card class="filter-card">
<el-form :inline="true" :model="filters">
<el-form-item :label="TEXT.common.fields.site">
<el-input v-model="filters.site_name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.site" clearable />
<el-select v-model="filters.center_id" :placeholder="TEXT.common.placeholders.select" clearable style="width: 200px">
<el-option
v-for="site in sites"
:key="site.id"
:label="site.name || TEXT.common.fallback"
:value="site.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.trackingNo">
<el-input v-model="filters.tracking_no" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.trackingNo" clearable />
<el-form-item :label="TEXT.common.fields.direction">
<el-select v-model="filters.direction" :placeholder="TEXT.common.placeholders.select" clearable style="width: 160px">
<el-option :label="TEXT.enums.shipmentDirection.SEND" value="SEND" />
<el-option :label="TEXT.enums.shipmentDirection.RETURN" value="RETURN" />
</el-select>
</el-form-item>
<el-form-item :label="TEXT.common.fields.status">
<el-select v-model="filters.status" :placeholder="TEXT.common.placeholders.select" clearable>
<el-select v-model="filters.status" :placeholder="TEXT.common.placeholders.select" clearable style="width: 160px">
<el-option :label="TEXT.enums.shipmentStatus.PENDING" value="PENDING" />
<el-option :label="TEXT.enums.shipmentStatus.IN_TRANSIT" value="IN_TRANSIT" />
<el-option :label="TEXT.enums.shipmentStatus.SIGNED" value="SIGNED" />
@@ -33,23 +43,42 @@
</el-card>
<el-card>
<el-table :data="items" v-loading="loading" style="width: 100%">
<el-table-column prop="direction" :label="TEXT.common.fields.direction" width="100">
<template #default="scope">{{ displayEnum(TEXT.enums.shipmentDirection, scope.row.direction) }}</template>
<el-table :data="items" v-loading="loading" style="width: 100%" @row-click="onRowClick">
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160">
<template #default="scope">{{ scope.row.site_name || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="direction" :label="TEXT.common.fields.direction" width="100">
<template #default="scope">
<el-tag :type="scope.row.direction === 'SEND' ? 'primary' : 'warning'" disable-transitions>
{{ displayEnum(TEXT.enums.shipmentDirection, scope.row.direction) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="site_name" :label="TEXT.common.fields.site" min-width="160" />
<el-table-column prop="tracking_no" :label="TEXT.common.fields.trackingNo" min-width="160" />
<el-table-column prop="ship_date" :label="TEXT.common.fields.shipDate" width="140">
<template #default="scope">{{ displayDate(scope.row.ship_date) }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<template #default="scope">{{ displayEnum(TEXT.enums.shipmentStatus, scope.row.status) }}</template>
<el-table-column prop="receive_date" :label="TEXT.common.fields.receiveDate" width="140">
<template #default="scope">{{ displayDate(scope.row.receive_date) }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="200">
<el-table-column prop="quantity" :label="TEXT.common.fields.quantity" width="120" align="right">
<template #default="scope">{{ typeof scope.row.quantity === "number" ? scope.row.quantity : TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="batch_no" :label="TEXT.common.fields.batchNo" min-width="140">
<template #default="scope">{{ scope.row.batch_no || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column prop="status" :label="TEXT.common.fields.status" width="120">
<template #default="scope">
<el-button link type="primary" size="small" @click="goDetail(scope.row.id)">{{ TEXT.common.actions.view }}</el-button>
<el-button link type="primary" size="small" @click="goEdit(scope.row.id)">{{ TEXT.common.actions.edit }}</el-button>
<el-button link type="danger" size="small" @click="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
<el-tag :type="statusType(scope.row.status)" disable-transitions>
{{ displayEnum(TEXT.enums.shipmentStatus, scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" :label="TEXT.common.fields.remark" min-width="160" show-overflow-tooltip>
<template #default="scope">{{ scope.row.remark || TEXT.common.fallback }}</template>
</el-table-column>
<el-table-column :label="TEXT.common.labels.actions" width="120" fixed="right">
<template #default="scope">
<el-button link type="danger" size="small" @click.stop="remove(scope.row)">{{ TEXT.common.actions.delete }}</el-button>
</template>
</el-table-column>
</el-table>
@@ -64,6 +93,7 @@ import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { useStudyStore } from "../../store/study";
import { listDrugShipments, deleteDrugShipment } from "../../api/drugShipments";
import { fetchSites } from "../../api/sites";
import { displayDate, displayEnum } from "../../utils/display";
import StateEmpty from "../../components/StateEmpty.vue";
import { TEXT } from "../../locales";
@@ -72,20 +102,32 @@ const router = useRouter();
const study = useStudyStore();
const loading = ref(false);
const items = ref<any[]>([]);
const sites = ref<any[]>([]);
const filters = reactive({
site_name: "",
tracking_no: "",
center_id: "",
direction: "",
status: "",
});
const loadSites = async () => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
try {
const { data } = await fetchSites(studyId, { limit: 500 });
sites.value = Array.isArray(data) ? data : data.items || [];
} catch {
sites.value = [];
}
};
const load = async () => {
const studyId = study.currentStudy?.id;
if (!studyId) return;
loading.value = true;
try {
const { data } = await listDrugShipments(studyId, {
site_name: filters.site_name || undefined,
tracking_no: filters.tracking_no || undefined,
center_id: filters.center_id || undefined,
direction: filters.direction || undefined,
status: filters.status || undefined,
});
items.value = Array.isArray(data) ? data : data.items || [];
@@ -97,15 +139,35 @@ const load = async () => {
};
const resetFilters = () => {
filters.site_name = "";
filters.tracking_no = "";
filters.center_id = "";
filters.direction = "";
filters.status = "";
load();
};
const goNew = () => router.push("/drug/shipments/new");
const goDetail = (id: string) => router.push(`/drug/shipments/${id}`);
const goEdit = (id: string) => router.push(`/drug/shipments/${id}/edit`);
const onRowClick = (row: any) => {
if (!row?.id) return;
goDetail(row.id);
};
const statusType = (status: string) => {
switch (status) {
case "PENDING":
return "info";
case "IN_TRANSIT":
return "primary";
case "SIGNED":
return "success";
case "RETURNED":
return "warning";
case "EXCEPTION":
return "danger";
default:
return "info";
}
};
const remove = async (row: any) => {
const studyId = study.currentStudy?.id;
@@ -121,7 +183,10 @@ const remove = async (row: any) => {
}
};
onMounted(load);
onMounted(async () => {
await loadSites();
load();
});
</script>
<style scoped>