|
|
@@ -0,0 +1,448 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import type { PrescriptionReviewModel } from '#/api';
|
|
|
+
|
|
|
+import { computed, onMounted, ref, watch } from 'vue';
|
|
|
+import { useRoute, useRouter } from 'vue-router';
|
|
|
+
|
|
|
+import { Page } from '@vben/common-ui';
|
|
|
+
|
|
|
+import {
|
|
|
+ Button,
|
|
|
+ Checkbox,
|
|
|
+ Input,
|
|
|
+ Radio,
|
|
|
+ RadioGroup,
|
|
|
+ Spin,
|
|
|
+ Textarea,
|
|
|
+} from 'ant-design-vue';
|
|
|
+
|
|
|
+import {
|
|
|
+ getArchivedPrescriptionReviewLogMethod,
|
|
|
+ listReviewStatisticsDetailIndicatorCategoriesMethod,
|
|
|
+ listReviewStatisticsDetailIndicatorsByCategoryMethod,
|
|
|
+} from '#/api';
|
|
|
+
|
|
|
+interface CategoryGroup {
|
|
|
+ categoryId: string;
|
|
|
+ categoryName: string;
|
|
|
+ indicators: PrescriptionReviewModel.ReviewIndicator[];
|
|
|
+}
|
|
|
+
|
|
|
+const router = useRouter();
|
|
|
+const route = useRoute();
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const logData = ref<PrescriptionReviewModel.ArchivedPrescriptionReviewLog>();
|
|
|
+const selectedEntryId = ref('');
|
|
|
+const indicatorGroups = ref<CategoryGroup[]>([]);
|
|
|
+
|
|
|
+const prescriptionId = computed(() => {
|
|
|
+ const id = route.params.id;
|
|
|
+ return Array.isArray(id) ? id[0] : id;
|
|
|
+});
|
|
|
+
|
|
|
+const prescription = computed(() => logData.value?.prescription);
|
|
|
+
|
|
|
+const entries = computed(() => logData.value?.entries ?? []);
|
|
|
+
|
|
|
+const currentEntry = computed(() =>
|
|
|
+ entries.value.find((item) => item.id === selectedEntryId.value),
|
|
|
+);
|
|
|
+
|
|
|
+const qualified = computed(
|
|
|
+ () => currentEntry.value?.qualified ?? prescription.value?.result === 'qualified',
|
|
|
+);
|
|
|
+
|
|
|
+const reviewComment = computed(() => currentEntry.value?.comment ?? '');
|
|
|
+
|
|
|
+const selectedIndicatorIds = computed(
|
|
|
+ () => currentEntry.value?.indicatorIds ?? [],
|
|
|
+);
|
|
|
+
|
|
|
+const herbIndicatorMap = computed(
|
|
|
+ () => currentEntry.value?.herbIndicatorMap ?? {},
|
|
|
+);
|
|
|
+
|
|
|
+function getHerbName(herb: string) {
|
|
|
+ return herb.split(' ')[0] ?? herb;
|
|
|
+}
|
|
|
+
|
|
|
+function getHerbIssueLabel(herb: string) {
|
|
|
+ const name = getHerbName(herb);
|
|
|
+ return prescription.value?.herbIssueLabels?.[name];
|
|
|
+}
|
|
|
+
|
|
|
+function isIndicatorChecked(indicatorId: string) {
|
|
|
+ return selectedIndicatorIds.value.includes(indicatorId);
|
|
|
+}
|
|
|
+
|
|
|
+function needsHerbInput(indicator: PrescriptionReviewModel.ReviewIndicator) {
|
|
|
+ return indicator.categoryId === 'cat-2' || indicator.associatedChineseMedicine;
|
|
|
+}
|
|
|
+
|
|
|
+function getSelectedHerbs(indicatorId: string) {
|
|
|
+ return herbIndicatorMap.value[indicatorId]?.join('、') ?? '';
|
|
|
+}
|
|
|
+
|
|
|
+async function loadIndicatorGroups() {
|
|
|
+ const categories = await listReviewStatisticsDetailIndicatorCategoriesMethod();
|
|
|
+ const groups = await Promise.all(
|
|
|
+ categories.map(async (category) => ({
|
|
|
+ categoryId: category.id,
|
|
|
+ categoryName: category.name,
|
|
|
+ indicators: await listReviewStatisticsDetailIndicatorsByCategoryMethod(
|
|
|
+ category.id,
|
|
|
+ ),
|
|
|
+ })),
|
|
|
+ );
|
|
|
+ indicatorGroups.value = groups.filter((item) => item.indicators.length > 0);
|
|
|
+}
|
|
|
+
|
|
|
+async function loadDetail() {
|
|
|
+ if (!prescriptionId.value) return;
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const [data] = await Promise.all([
|
|
|
+ getArchivedPrescriptionReviewLogMethod(prescriptionId.value),
|
|
|
+ loadIndicatorGroups(),
|
|
|
+ ]);
|
|
|
+ logData.value = data;
|
|
|
+ selectedEntryId.value = data.entries[0]?.id ?? '';
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function goBack() {
|
|
|
+ router.push('/prescription-review/archived/list');
|
|
|
+}
|
|
|
+
|
|
|
+function selectEntry(entry: PrescriptionReviewModel.ArchivedPrescriptionReviewLogEntry) {
|
|
|
+ selectedEntryId.value = entry.id;
|
|
|
+}
|
|
|
+
|
|
|
+watch(prescriptionId, loadDetail);
|
|
|
+
|
|
|
+onMounted(loadDetail);
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <Page auto-content-height class="archived-log-page">
|
|
|
+ <Spin :spinning="loading">
|
|
|
+ <div class="detail-header">
|
|
|
+ <Button type="link" @click="goBack">返回</Button>
|
|
|
+ </div>
|
|
|
+ <div v-if="prescription" class="detail-body">
|
|
|
+ <div class="prescription-panel">
|
|
|
+ <div class="panel-title">处方详情</div>
|
|
|
+ <div class="info-grid">
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">医疗机构:</span>
|
|
|
+ <span>{{ prescription.institutionName }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">科室:</span>
|
|
|
+ <span>{{ prescription.departmentName }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">医生:</span>
|
|
|
+ <span>{{ prescription.doctorName }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">药师:</span>
|
|
|
+ <span>{{ prescription.pharmacistName || '—' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">处方号:</span>
|
|
|
+ <a class="text-primary">{{ prescription.prescriptionNo }}</a>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">姓名:</span>
|
|
|
+ <span>{{ prescription.patientName }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">性别:</span>
|
|
|
+ <span>{{ prescription.patientGender }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">年龄:</span>
|
|
|
+ <span>{{ prescription.patientAge }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">中医病名:</span>
|
|
|
+ <span>{{ prescription.tcmDisease }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">中医证候:</span>
|
|
|
+ <span>{{ prescription.tcmSyndrome }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">治则治法:</span>
|
|
|
+ <span>{{ prescription.treatmentPrinciple }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">服法:</span>
|
|
|
+ <span>{{ prescription.administrationMethod }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">药味数:</span>
|
|
|
+ <span>{{ prescription.herbCount }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">妊娠:</span>
|
|
|
+ <span>{{ prescription.pregnancy ? '是' : '否' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">哺乳:</span>
|
|
|
+ <span>{{ prescription.lactation ? '是' : '否' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">剂数:</span>
|
|
|
+ <span>{{ prescription.doseCount }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">总金额:</span>
|
|
|
+ <span>{{ prescription.totalAmount }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <span class="info-label">单剂金额:</span>
|
|
|
+ <span>{{ prescription.unitDoseAmount }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="herb-section">
|
|
|
+ <div class="herb-title">明细</div>
|
|
|
+ <div class="herb-grid">
|
|
|
+ <span
|
|
|
+ v-for="(herb, idx) in prescription.herbs"
|
|
|
+ :key="idx"
|
|
|
+ class="herb-item"
|
|
|
+ :class="{ 'herb-item--issue': getHerbIssueLabel(herb) }"
|
|
|
+ >
|
|
|
+ {{ herb }}
|
|
|
+ <span v-if="getHerbIssueLabel(herb)" class="herb-issue">
|
|
|
+ {{ getHerbIssueLabel(herb) }}
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="entries.length > 1" class="timeline-panel">
|
|
|
+ <div
|
|
|
+ v-for="entry in entries"
|
|
|
+ :key="entry.id"
|
|
|
+ class="timeline-item"
|
|
|
+ :class="{ 'timeline-item--active': entry.id === selectedEntryId }"
|
|
|
+ @click="selectEntry(entry)"
|
|
|
+ >
|
|
|
+ {{ entry.reviewDate }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="review-panel">
|
|
|
+ <div class="panel-title">点评结果</div>
|
|
|
+ <div class="review-status-row">
|
|
|
+ <RadioGroup :value="qualified">
|
|
|
+ <Radio :value="true">合格</Radio>
|
|
|
+ <Radio :value="false">不合格</Radio>
|
|
|
+ </RadioGroup>
|
|
|
+ </div>
|
|
|
+ <div v-if="currentEntry" class="review-meta">
|
|
|
+ <span>抽样名称:{{ currentEntry.samplingName }}</span>
|
|
|
+ <span>抽样时间:{{ currentEntry.samplingTime }}</span>
|
|
|
+ <span>点评专家:{{ currentEntry.reviewExpert || '—' }}</span>
|
|
|
+ <span>点评时间:{{ currentEntry.reviewTime || '—' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="comment-section">
|
|
|
+ <div class="comment-label">点评意见和说明</div>
|
|
|
+ <Textarea
|
|
|
+ :rows="4"
|
|
|
+ :value="reviewComment"
|
|
|
+ disabled
|
|
|
+ placeholder="暂无点评意见"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-for="group in indicatorGroups"
|
|
|
+ :key="group.categoryId"
|
|
|
+ class="indicator-group"
|
|
|
+ >
|
|
|
+ <div class="group-title">{{ group.categoryName }}</div>
|
|
|
+ <div class="group-items">
|
|
|
+ <div
|
|
|
+ v-for="indicator in group.indicators"
|
|
|
+ :key="indicator.id"
|
|
|
+ class="indicator-item"
|
|
|
+ >
|
|
|
+ <Checkbox
|
|
|
+ :checked="isIndicatorChecked(indicator.id)"
|
|
|
+ disabled
|
|
|
+ >
|
|
|
+ {{ indicator.name }}
|
|
|
+ </Checkbox>
|
|
|
+ <Input
|
|
|
+ v-if="
|
|
|
+ isIndicatorChecked(indicator.id) &&
|
|
|
+ needsHerbInput(indicator)
|
|
|
+ "
|
|
|
+ :value="getSelectedHerbs(indicator.id)"
|
|
|
+ class="herb-input"
|
|
|
+ disabled
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Spin>
|
|
|
+ </Page>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.detail-header {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-body {
|
|
|
+ display: flex;
|
|
|
+ gap: 0;
|
|
|
+ min-height: calc(100vh - 180px);
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.prescription-panel {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ padding: 16px 20px;
|
|
|
+ border-right: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.timeline-panel {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ flex-shrink: 0;
|
|
|
+ gap: 8px;
|
|
|
+ width: 88px;
|
|
|
+ padding: 16px 8px;
|
|
|
+ border-right: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.timeline-item {
|
|
|
+ padding: 6px 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.4;
|
|
|
+ color: rgb(0 0 0 / 65%);
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.timeline-item--active {
|
|
|
+ color: #1677ff;
|
|
|
+ background: #e6f4ff;
|
|
|
+}
|
|
|
+
|
|
|
+.review-panel {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ padding: 16px 20px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-title {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.info-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 10px 24px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-label {
|
|
|
+ color: rgb(0 0 0 / 65%);
|
|
|
+}
|
|
|
+
|
|
|
+.herb-section {
|
|
|
+ padding: 12px;
|
|
|
+ border: 1px solid #d9d9d9;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.herb-title {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.herb-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 8px 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.herb-item {
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.herb-item--issue {
|
|
|
+ color: #ff4d4f;
|
|
|
+}
|
|
|
+
|
|
|
+.herb-issue {
|
|
|
+ margin-left: 4px;
|
|
|
+ color: #ff4d4f;
|
|
|
+}
|
|
|
+
|
|
|
+.review-status-row {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.review-meta {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgb(0 0 0 / 65%);
|
|
|
+}
|
|
|
+
|
|
|
+.comment-section {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.comment-label {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgb(0 0 0 / 65%);
|
|
|
+}
|
|
|
+
|
|
|
+.indicator-group {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.group-title {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.group-items {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.indicator-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.herb-input {
|
|
|
+ width: 100px;
|
|
|
+}
|
|
|
+</style>
|