feat(tcm): add TCM diagnosis entity, saveDiagnosis/saveConstitution endpoints, and unified TcmDiagnosis.vue

This commit is contained in:
2026-06-18 15:54:02 +08:00
parent 4a45c9cdd4
commit 5aaa4ee883
9 changed files with 399 additions and 2 deletions

View File

@@ -8,4 +8,7 @@ public interface ITcmAppService {
TcmConstitutionAssessment assess(TcmConstitutionAssessment a);
List<TcmConstitutionAssessment> getAssessmentsByEncounter(Long encounterId);
Map<String, Object> getStatistics();
TcmDiagnosis saveDiagnosis(TcmDiagnosis d);
List<TcmDiagnosis> getDiagnosesByEncounter(Long encounterId);
TcmConstitutionAssessment saveConstitution(TcmConstitutionAssessment a);
}

View File

@@ -10,6 +10,7 @@ import java.util.*;
public class TcmAppServiceImpl implements ITcmAppService {
@Autowired private ITcmPrescriptionService prescriptionService;
@Autowired private ITcmConstitutionAssessmentService assessmentService;
@Autowired private ITcmDiagnosisService diagnosisService;
@Override
public List<TcmPrescription> getPrescriptions(String type) {
@@ -33,4 +34,13 @@ public class TcmAppServiceImpl implements ITcmAppService {
r.put("totalAssessments", assessmentService.count(new LambdaQueryWrapper<TcmConstitutionAssessment>().eq(TcmConstitutionAssessment::getDeleteFlag, "0")));
return r;
}
@Override
public TcmDiagnosis saveDiagnosis(TcmDiagnosis d) { d.setDeleteFlag("0"); d.setEnabled("1"); diagnosisService.save(d); return d; }
@Override
public List<TcmDiagnosis> getDiagnosesByEncounter(Long encounterId) {
return diagnosisService.list(new LambdaQueryWrapper<TcmDiagnosis>()
.eq(TcmDiagnosis::getEncounterId, encounterId).eq(TcmDiagnosis::getDeleteFlag, "0"));
}
@Override
public TcmConstitutionAssessment saveConstitution(TcmConstitutionAssessment a) { a.setDeleteFlag("0"); assessmentService.save(a); return a; }
}

View File

@@ -5,19 +5,37 @@ import com.healthlink.his.web.tcm.appservice.ITcmAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "壮医中医特色") @RestController @RequestMapping("/api/v1/tcm")
public class TcmController {
@Autowired private ITcmAppService tcmAppService;
@Operation(summary = "中医方剂列表") @GetMapping("/prescriptions")
@PreAuthorize("hasAuthority('tcm:edit')")
public AjaxResult prescriptions(@RequestParam(required = false) String type) { return AjaxResult.success(tcmAppService.getPrescriptions(type)); }
@Operation(summary = "新增方剂") @PostMapping("/prescription")
@PreAuthorize("hasAuthority('tcm:edit')")
public AjaxResult savePrescription(@RequestBody TcmPrescription p) { return AjaxResult.success(tcmAppService.savePrescription(p)); }
@Operation(summary = "统计") @GetMapping("/statistics")
@PreAuthorize("hasAuthority('tcm:list')")
public AjaxResult statistics() { return AjaxResult.success(tcmAppService.getStatistics()); }
@Operation(summary = "体质辨识") @PostMapping("/constitution")
@PreAuthorize("hasAuthority('tcm:edit')")
public AjaxResult assess(@RequestBody TcmConstitutionAssessment a) { return AjaxResult.success(tcmAppService.assess(a)); }
@Operation(summary = "体质辨识记录") @GetMapping("/constitution/encounter/{encounterId}")
@PreAuthorize("hasAuthority('tcm:list')")
public AjaxResult getAssessments(@PathVariable Long encounterId) { return AjaxResult.success(tcmAppService.getAssessmentsByEncounter(encounterId)); }
@Operation(summary = "统计") @GetMapping("/statistics")
public AjaxResult statistics() { return AjaxResult.success(tcmAppService.getStatistics()); }
@Operation(summary = "保存体质辨识") @PostMapping("/constitution/save")
@PreAuthorize("hasAuthority('tcm:edit')")
public AjaxResult saveConstitution(@RequestBody TcmConstitutionAssessment a) { return AjaxResult.success(tcmAppService.saveConstitution(a)); }
@Operation(summary = "保存诊断") @PostMapping("/diagnosis")
@PreAuthorize("hasAuthority('tcm:edit')")
public AjaxResult saveDiagnosis(@RequestBody TcmDiagnosis d) { return AjaxResult.success(tcmAppService.saveDiagnosis(d)); }
@Operation(summary = "诊断记录") @GetMapping("/diagnosis/encounter/{encounterId}")
@PreAuthorize("hasAuthority('tcm:list')")
public AjaxResult getDiagnoses(@PathVariable Long encounterId) { return AjaxResult.success(tcmAppService.getDiagnosesByEncounter(encounterId)); }
}

View File

@@ -0,0 +1,22 @@
-- TCM diagnosis table
CREATE TABLE IF NOT EXISTS healthlink_his.tcm_diagnosis (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT,
patient_id BIGINT,
diagnosis_type VARCHAR(50),
diagnosis_name VARCHAR(200),
diagnosis_code VARCHAR(50),
syndrome_type VARCHAR(50),
syndrome_name VARCHAR(200),
syndrome_code VARCHAR(50),
remark TEXT,
enabled CHAR(1) DEFAULT '1',
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tcm_diagnosis_encounter ON healthlink_his.tcm_diagnosis(encounter_id);

View File

@@ -0,0 +1,28 @@
package com.healthlink.his.tcm.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@TableName("tcm_diagnosis")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class TcmDiagnosis extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long patientId;
private String diagnosisType;
private String diagnosisName;
private String diagnosisCode;
private String syndromeType;
private String syndromeName;
private String syndromeCode;
private String remark;
private String enabled;
}

View File

@@ -0,0 +1,5 @@
package com.healthlink.his.tcm.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.tcm.domain.TcmDiagnosis;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface TcmDiagnosisMapper extends BaseMapper<TcmDiagnosis> {}

View File

@@ -0,0 +1,4 @@
package com.healthlink.his.tcm.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.tcm.domain.TcmDiagnosis;
public interface ITcmDiagnosisService extends IService<TcmDiagnosis> {}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.tcm.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.tcm.domain.TcmDiagnosis;
import com.healthlink.his.tcm.mapper.TcmDiagnosisMapper;
import com.healthlink.his.tcm.service.ITcmDiagnosisService;
import org.springframework.stereotype.Service;
@Service public class TcmDiagnosisServiceImpl extends ServiceImpl<TcmDiagnosisMapper, TcmDiagnosis> implements ITcmDiagnosisService {}

View File

@@ -0,0 +1,300 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="中医诊断" name="diagnosis">
<el-form :inline="true" :model="diagQuery" label-width="100px">
<el-form-item label="就诊号">
<el-input v-model="diagQuery.encounterId" placeholder="请输入就诊号" clearable style="width:180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="loadDiagnoses">查询</el-button>
<el-button type="success" icon="Plus" @click="showDiagDialog()">新增诊断</el-button>
</el-form-item>
</el-form>
<el-table v-loading="diagLoading" :data="diagList" border>
<el-table-column label="就诊号" align="center" prop="encounterId" width="140" />
<el-table-column label="诊断类型" align="center" prop="diagnosisType" width="120">
<template #default="{ row }">
<el-tag>{{ diagTypeText(row.diagnosisType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="诊断名称" align="center" prop="diagnosisName" min-width="160" show-overflow-tooltip />
<el-table-column label="证候类型" align="center" prop="syndromeType" width="120">
<template #default="{ row }">
<el-tag v-if="row.syndromeType">{{ syndromeTypeText(row.syndromeType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="证候名称" align="center" prop="syndromeName" min-width="160" show-overflow-tooltip />
<el-table-column label="备注" align="center" prop="remark" min-width="180" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<el-tab-pane label="方剂管理" name="prescription">
<el-form :inline="true" :model="rxQuery" label-width="100px">
<el-form-item label="方剂类型">
<el-select v-model="rxQuery.type" placeholder="请选择" clearable style="width:160px">
<el-option label="经典方剂" value="CLASSIC" />
<el-option label="经验方" value="EXPERIENCE" />
<el-option label="协定方" value="AGREEMENT" />
<el-option label="壮医方" value="ZHUANG" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="loadPrescriptions">搜索</el-button>
<el-button type="success" icon="Plus" @click="showRxDialog()">新增方剂</el-button>
</el-form-item>
</el-form>
<el-table v-loading="rxLoading" :data="rxList" border>
<el-table-column label="方剂名称" align="center" prop="prescriptionName" min-width="160" show-overflow-tooltip />
<el-table-column label="方剂类型" align="center" prop="prescriptionType" width="120">
<template #default="{ row }">
<el-tag>{{ rxTypeText(row.prescriptionType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="组成" align="center" prop="herbs" min-width="200" show-overflow-tooltip />
<el-table-column label="主治" align="center" prop="indication" min-width="150" show-overflow-tooltip />
<el-table-column label="用法用量" align="center" prop="dosage" width="120" />
<el-table-column label="来源" align="center" prop="source" width="120" />
</el-table>
</el-tab-pane>
<el-tab-pane label="体质辨识" name="constitution">
<el-form :inline="true" :model="constQuery" label-width="100px">
<el-form-item label="就诊号">
<el-input v-model="constQuery.encounterId" placeholder="请输入就诊号" clearable style="width:180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="loadConstitutions">查询</el-button>
<el-button type="success" icon="Plus" @click="showConstDialog()">新增评估</el-button>
</el-form-item>
</el-form>
<el-table v-loading="constLoading" :data="constList" border>
<el-table-column label="就诊号" align="center" prop="encounterId" width="140" />
<el-table-column label="体质类型" align="center" prop="constitutionType" width="140">
<template #default="{ row }">
<el-tag>{{ constTypeText(row.constitutionType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="评估得分" align="center" prop="score" width="100" />
<el-table-column label="调理建议" align="center" prop="recommendation" min-width="200" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<el-tab-pane label="统计" name="statistics">
<el-descriptions :column="3" border>
<el-descriptions-item label="方剂总数">{{ stats.totalPrescriptions || 0 }}</el-descriptions-item>
<el-descriptions-item label="体质辨识总数">{{ stats.totalAssessments || 0 }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="diagDialogVisible" title="中医诊断" width="600px" append-to-body>
<el-form :model="diagForm" label-width="110px">
<el-form-item label="就诊号">
<el-input v-model="diagForm.encounterId" placeholder="请输入就诊号" />
</el-form-item>
<el-form-item label="诊断类型">
<el-select v-model="diagForm.diagnosisType" placeholder="请选择">
<el-option label="中医主病诊断" value="MAIN_DISEASE" />
<el-option label="中医主证诊断" value="MAIN_SYNDROME" />
</el-select>
</el-form-item>
<el-form-item label="诊断名称">
<el-input v-model="diagForm.diagnosisName" placeholder="请输入诊断名称" />
</el-form-item>
<el-form-item label="诊断编码">
<el-input v-model="diagForm.diagnosisCode" placeholder="请输入诊断编码" />
</el-form-item>
<el-form-item label="证候类型">
<el-select v-model="diagForm.syndromeType" placeholder="请选择">
<el-option label="风证" value="WIND" />
<el-option label="寒证" value="COLD" />
<el-option label="暑证" value="HEAT" />
<el-option label="湿证" value="DAMP" />
<el-option label="燥证" value="DRY" />
<el-option label="火证" value="FIRE" />
</el-select>
</el-form-item>
<el-form-item label="证候名称">
<el-input v-model="diagForm.syndromeName" placeholder="请输入证候名称" />
</el-form-item>
<el-form-item label="证候编码">
<el-input v-model="diagForm.syndromeCode" placeholder="请输入证候编码" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="diagForm.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="diagDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitDiag">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="rxDialogVisible" title="新增方剂" width="600px" append-to-body>
<el-form :model="rxForm" label-width="100px">
<el-form-item label="方剂名称">
<el-input v-model="rxForm.prescriptionName" placeholder="请输入方剂名称" />
</el-form-item>
<el-form-item label="方剂类型">
<el-select v-model="rxForm.prescriptionType" placeholder="请选择">
<el-option label="经典方剂" value="CLASSIC" />
<el-option label="经验方" value="EXPERIENCE" />
<el-option label="协定方" value="AGREEMENT" />
<el-option label="壮医方" value="ZHUANG" />
</el-select>
</el-form-item>
<el-form-item label="组成">
<el-input v-model="rxForm.herbs" type="textarea" :rows="3" placeholder="请输入药物组成" />
</el-form-item>
<el-form-item label="主治">
<el-input v-model="rxForm.indication" placeholder="请输入主治病症" />
</el-form-item>
<el-form-item label="用法用量">
<el-input v-model="rxForm.dosage" placeholder="请输入用法用量" />
</el-form-item>
<el-form-item label="来源">
<el-input v-model="rxForm.source" placeholder="请输入方剂来源" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rxDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRx">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="constDialogVisible" title="体质辨识" width="600px" append-to-body>
<el-form :model="constForm" label-width="110px">
<el-form-item label="就诊号">
<el-input v-model="constForm.encounterId" placeholder="请输入就诊号" />
</el-form-item>
<el-form-item label="体质类型">
<el-select v-model="constForm.constitutionType" placeholder="请选择">
<el-option v-for="item in constitutionTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="评估得分">
<el-input-number v-model="constForm.score" :min="0" :max="100" />
</el-form-item>
<el-form-item label="调理建议">
<el-input v-model="constForm.recommendation" type="textarea" :rows="3" placeholder="请输入调理建议" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="constDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitConst">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="TcmDiagnosis" lang="js">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const activeTab = ref('diagnosis')
const diagLoading = ref(false)
const rxLoading = ref(false)
const constLoading = ref(false)
const diagDialogVisible = ref(false)
const rxDialogVisible = ref(false)
const constDialogVisible = ref(false)
const diagList = ref([])
const rxList = ref([])
const constList = ref([])
const stats = ref({})
const diagQuery = reactive({ encounterId: '' })
const rxQuery = reactive({ type: undefined })
const constQuery = reactive({ encounterId: '' })
const diagForm = reactive({ encounterId: '', diagnosisType: 'MAIN_DISEASE', diagnosisName: '', diagnosisCode: '', syndromeType: '', syndromeName: '', syndromeCode: '', remark: '' })
const rxForm = reactive({ prescriptionName: '', prescriptionType: 'CLASSIC', herbs: '', indication: '', dosage: '', source: '' })
const constForm = reactive({ encounterId: '', constitutionType: 'PINGHE', score: 0, recommendation: '' })
const constitutionTypes = [
{ value: 'PINGHE', label: '平和质' }, { value: 'QIXU', label: '气虚质' },
{ value: 'YANGXU', label: '阳虚质' }, { value: 'YINXU', label: '阴虚质' },
{ value: 'TANSHI', label: '痰湿质' }, { value: 'SHIRE', label: '湿热质' },
{ value: 'XUEYU', label: '血瘀质' }, { value: 'QIYU', label: '气郁质' },
{ value: 'TEBING', label: '特禀质' }
]
function diagTypeText(t) {
return { MAIN_DISEASE: '中医主病诊断', MAIN_SYNDROME: '中医主证诊断' }[t] || t
}
function syndromeTypeText(t) {
return { WIND: '风证', COLD: '寒证', HEAT: '暑证', DAMP: '湿证', DRY: '燥证', FIRE: '火证' }[t] || t
}
function rxTypeText(t) {
return { CLASSIC: '经典方剂', EXPERIENCE: '经验方', AGREEMENT: '协定方', ZHUANG: '壮医方' }[t] || t
}
function constTypeText(t) {
const m = { PINGHE: '平和质', QIXU: '气虚质', YANGXU: '阳虚质', YINXU: '阴虚质', TANSHI: '痰湿质', SHIRE: '湿热质', XUEYU: '血瘀质', QIYU: '气郁质', TEBING: '特禀质' }
return m[t] || t
}
function loadDiagnoses() {
diagLoading.value = true
const url = diagQuery.encounterId
? `/api/v1/tcm/diagnosis/encounter/${diagQuery.encounterId}`
: '/api/v1/tcm/diagnosis/encounter/0'
request({ url, method: 'get' }).then(res => {
diagList.value = Array.isArray(res.data) ? res.data : []
}).catch(() => { diagList.value = [] }).finally(() => { diagLoading.value = false })
}
function loadPrescriptions() {
rxLoading.value = true
request({ url: '/api/v1/tcm/prescriptions', method: 'get', params: rxQuery }).then(res => {
rxList.value = res.data || []
}).finally(() => { rxLoading.value = false })
}
function loadConstitutions() {
constLoading.value = true
const url = constQuery.encounterId
? `/api/v1/tcm/constitution/encounter/${constQuery.encounterId}`
: '/api/v1/tcm/statistics'
request({ url, method: 'get' }).then(res => {
constList.value = Array.isArray(res.data) ? res.data : (res.data?.records || [])
}).catch(() => { constList.value = [] }).finally(() => { constLoading.value = false })
}
function loadStats() {
request({ url: '/api/v1/tcm/statistics', method: 'get' }).then(res => { stats.value = res.data || {} })
}
function showDiagDialog(row) {
if (row) { Object.assign(diagForm, row) } else { Object.assign(diagForm, { encounterId: '', diagnosisType: 'MAIN_DISEASE', diagnosisName: '', diagnosisCode: '', syndromeType: '', syndromeName: '', syndromeCode: '', remark: '' }) }
diagDialogVisible.value = true
}
function showRxDialog() { Object.assign(rxForm, { prescriptionName: '', prescriptionType: 'CLASSIC', herbs: '', indication: '', dosage: '', source: '' }); rxDialogVisible.value = true }
function showConstDialog() { Object.assign(constForm, { encounterId: '', constitutionType: 'PINGHE', score: 0, recommendation: '' }); constDialogVisible.value = true }
function submitDiag() {
request({ url: '/api/v1/tcm/diagnosis', method: 'post', data: diagForm }).then(() => {
ElMessage.success('诊断保存成功'); diagDialogVisible.value = false; loadDiagnoses()
})
}
function submitRx() {
request({ url: '/api/v1/tcm/prescription', method: 'post', data: rxForm }).then(() => {
ElMessage.success('方剂保存成功'); rxDialogVisible.value = false; loadPrescriptions()
})
}
function submitConst() {
request({ url: '/api/v1/tcm/constitution/save', method: 'post', data: constForm }).then(() => {
ElMessage.success('评估保存成功'); constDialogVisible.value = false; loadConstitutions()
})
}
function handleTabChange() {
if (activeTab.value === 'diagnosis') loadDiagnoses()
else if (activeTab.value === 'prescription') loadPrescriptions()
else if (activeTab.value === 'constitution') loadConstitutions()
else if (activeTab.value === 'statistics') loadStats()
}
onMounted(() => { loadDiagnoses(); loadStats() })
</script>