feat: VIP功能绑定、VIP病历模块、字典模块、传方按钮移动到顶部导航栏

This commit is contained in:
李琦
2026-08-05 10:45:58 +08:00
parent 212b16a8ec
commit 029b45d0bb
43 changed files with 5821 additions and 153 deletions

View File

@@ -0,0 +1,27 @@
/**
* 疾病诊断字典 API(yii_disease)
*/
import { requestClient } from '#/api/request';
const prefix = 'disease-dict/';
export async function getDiseaseDictList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
export async function createDiseaseDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
export async function updateDiseaseDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
export async function deleteDiseaseDict(data: { ids: number[] }) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/** 同步空拼音首拼/全拼 */
export async function syncDiseaseDictPinyin() {
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
}

View File

@@ -0,0 +1,269 @@
<script lang="ts" setup>
/**
* 疾病诊断字典(yii_disease)
* 挂在字典管理下,维护接诊诊断库
*/
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Modal as AntModal } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import {
createDiseaseDict,
deleteDiseaseDict,
getDiseaseDictList,
syncDiseaseDictPinyin,
updateDiseaseDict,
} from './api';
defineOptions({ name: 'DiseaseDict' });
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
const isUpdate = ref(false);
const formOptions = {
schema: [
{
component: 'VbenInput',
fieldName: 'keyword',
label: '关键字',
componentProps: { placeholder: '名称/代码/首拼/全拼' },
},
],
};
const gridOptions = {
checkboxConfig: { highlight: true, labelField: '' },
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', title: 'ID', width: 80 },
{ field: 'diagnose_code', title: '诊断代码', width: 120 },
{ field: 'name', title: '诊断名称', minWidth: 180 },
{ field: 'pinyin', title: '首拼', width: 120 },
{ field: 'pinyin_full', title: '全拼', minWidth: 160 },
{ field: 'major_number', title: '主编号', width: 120 },
{ field: 'ref_number', title: '次编号', width: 120 },
{ field: 'created_at', title: '创建时间', width: 170 },
{ title: '操作', slots: { default: 'action' }, width: 140, fixed: 'right' },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }: any, formValues: any) => {
return await getDiseaseDictList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
toolbarConfig: {
search: true,
refresh: true,
slots: { buttons: 'toolbar-buttons' },
},
};
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
hasTopTableDropDownActions.value =
gridApi.grid.getCheckboxRecords().length > 0;
},
checkboxAll() {
hasTopTableDropDownActions.value =
gridApi.grid.getCheckboxRecords().length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions: gridOptions as any,
gridEvents,
});
const [Form, formApi] = useVbenForm({
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: { class: 'w-full' },
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: { show: false, triggerFields: ['id'] },
},
{
component: 'VbenInput',
fieldName: 'name',
label: '诊断名称',
rules: 'required',
},
{
component: 'VbenInput',
fieldName: 'diagnose_code',
label: '诊断代码',
formItemClass: 'col-span-6',
},
{
component: 'VbenInput',
fieldName: 'major_number',
label: '主编号',
formItemClass: 'col-span-6',
},
{
component: 'VbenInput',
fieldName: 'ref_number',
label: '次编号',
formItemClass: 'col-span-6',
},
{
component: 'VbenInput',
fieldName: 'pinyin',
label: '拼音首拼',
formItemClass: 'col-span-6',
componentProps: { placeholder: '可留空,保存时按名称自动生成' },
},
{
component: 'VbenInput',
fieldName: 'pinyin_full',
label: '拼音全拼',
componentProps: { placeholder: '可留空,保存时按名称自动生成' },
},
],
showDefaultActions: false,
});
const [FormModal, formModalApi] = useVbenModal({
class: 'w-[640px]',
draggable: true,
onConfirm: async () => {
const e = await formApi.validate();
if (!e.valid) return;
const values = await formApi.getValues();
formModalApi.setState({ confirmLoading: true });
try {
if (isUpdate.value) {
await updateDiseaseDict(values);
} else {
await createDiseaseDict(values);
}
message.success('保存成功');
gridApi.query();
formModalApi.close();
} finally {
formModalApi.setState({ confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
formApi.resetForm();
return;
}
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
isUpdate.value = !!data?.update;
if (data?.values && data.update) {
formApi.setValues({ ...data.values });
} else {
formApi.resetForm();
}
},
});
function showModal(row: any = {}, update = false) {
formModalApi.setData({ values: row, update });
formModalApi.open();
}
async function handleDelete(ids: number[]) {
await deleteDiseaseDict({ ids });
message.success('删除成功');
gridApi.query();
}
/** 手动同步空拼音(疾病库量大,可能稍慢) */
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content:
'将扫描疾病诊断中首拼或全拼为空的记录并自动生成,已有值不会覆盖。确认执行?',
onOk: async () => {
syncing.value = true;
try {
const res = await syncDiseaseDictPinyin();
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)} 条`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
</script>
<template>
<Page auto-content-height title="疾病诊断">
<FormModal :title="`${isUpdate ? '编辑' : '新增'}疾病诊断`">
<Form />
</FormModal>
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
onClick: () => showModal({}, false),
},
{
label: '批量删除',
danger: true,
ifShow: () => hasTopTableDropDownActions,
popConfirm: {
title: '确认删除所选?',
confirm: () => {
const ids = gridApi.grid
.getCheckboxRecords()
.map((r: any) => r.id);
handleDelete(ids);
},
},
},
]"
/>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #action="{ row }">
<TableAction
:actions="[
{ label: '编辑', onClick: () => showModal(row, true) },
{
label: '删除',
danger: true,
popConfirm: {
title: '确认删除?',
confirm: () => handleDelete([row.id]),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,27 @@
/**
* 公共医嘱字典 API(xk_doctor_order)
*/
import { requestClient } from '#/api/request';
const prefix = 'doctor-order/';
export async function getDoctorOrderDictList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
export async function createDoctorOrderDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
export async function updateDoctorOrderDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
export async function deleteDoctorOrderDict(data: { ids?: number[]; id?: number }) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/** 同步空拼音首拼/全拼 */
export async function syncDoctorOrderDictPinyin() {
return requestClient.post<any>(`${prefix}sync-pinyin`, {});
}

View File

@@ -0,0 +1,235 @@
<script lang="ts" setup>
/**
* 公共医嘱字典(xk_doctor_order)
* 挂在字典管理下,维护接诊公共医嘱
*/
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Modal as AntModal } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import {
createDoctorOrderDict,
deleteDoctorOrderDict,
getDoctorOrderDictList,
syncDoctorOrderDictPinyin,
updateDoctorOrderDict,
} from './api';
defineOptions({ name: 'DoctorOrderDict' });
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
const isUpdate = ref(false);
const formOptions = {
schema: [
{
component: 'VbenInput',
fieldName: 'keyword',
label: '关键字',
componentProps: { placeholder: '内容/首拼/全拼' },
},
],
};
const gridOptions = {
checkboxConfig: { highlight: true, labelField: '' },
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', title: 'ID', width: 80 },
{ field: 'name', title: '医嘱内容', minWidth: 220 },
{ field: 'pinyin_initials', title: '首拼', width: 120 },
{ field: 'pinyin_full', title: '全拼', minWidth: 160 },
{ field: 'created_at', title: '创建时间', width: 170 },
{ title: '操作', slots: { default: 'action' }, width: 140, fixed: 'right' },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }: any, formValues: any) => {
return await getDoctorOrderDictList({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
toolbarConfig: {
search: true,
refresh: true,
slots: { buttons: 'toolbar-buttons' },
},
};
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
hasTopTableDropDownActions.value =
gridApi.grid.getCheckboxRecords().length > 0;
},
checkboxAll() {
hasTopTableDropDownActions.value =
gridApi.grid.getCheckboxRecords().length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions: gridOptions as any,
gridEvents,
});
const [Form, formApi] = useVbenForm({
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: { class: 'w-full' },
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: { show: false, triggerFields: ['id'] },
},
{
component: 'VbenInput',
fieldName: 'name',
label: '医嘱内容',
rules: 'required',
componentProps: { placeholder: '请输入医嘱内容' },
},
],
showDefaultActions: false,
});
const [FormModal, formModalApi] = useVbenModal({
class: 'w-[560px]',
draggable: true,
onConfirm: async () => {
const e = await formApi.validate();
if (!e.valid) return;
const values = await formApi.getValues();
formModalApi.setState({ confirmLoading: true });
try {
if (isUpdate.value) {
await updateDoctorOrderDict(values);
} else {
await createDoctorOrderDict(values);
}
message.success('保存成功');
gridApi.query();
formModalApi.close();
} finally {
formModalApi.setState({ confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
formApi.resetForm();
return;
}
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
isUpdate.value = !!data?.update;
if (data?.values && data.update) {
formApi.setValues({ ...data.values });
} else {
formApi.resetForm();
}
},
});
function showModal(row: any = {}, update = false) {
formModalApi.setData({ values: row, update });
formModalApi.open();
}
async function handleDelete(ids: number[]) {
await deleteDoctorOrderDict({ ids });
message.success('删除成功');
gridApi.query();
}
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content:
'将扫描公共医嘱中首拼或全拼为空的记录并自动生成,已有值不会覆盖。确认执行?',
onOk: async () => {
syncing.value = true;
try {
const res = await syncDoctorOrderDictPinyin();
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)} 条`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
</script>
<template>
<Page auto-content-height title="公共医嘱">
<FormModal :title="`${isUpdate ? '编辑' : '新增'}公共医嘱`">
<Form />
</FormModal>
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
onClick: () => showModal({}, false),
},
{
label: '批量删除',
danger: true,
ifShow: () => hasTopTableDropDownActions,
popConfirm: {
title: '确认删除所选?',
confirm: () => {
const ids = gridApi.grid
.getCheckboxRecords()
.map((r: any) => r.id);
handleDelete(ids);
},
},
},
]"
/>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #action="{ row }">
<TableAction
:actions="[
{ label: '编辑', onClick: () => showModal(row, true) },
{
label: '删除',
danger: true,
popConfirm: {
title: '确认删除?',
confirm: () => handleDelete([row.id]),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,34 @@
/**
* 中医字典 API(疾病 / 治法 / 证候)
*/
import { requestClient } from '#/api/request';
const prefix = 'tcm-dict/';
export type TcmDictType = 'diseases' | 'method' | 'syndrome';
export async function getTcmDictList(data: {
type: TcmDictType;
page?: number;
pageSize?: number;
keyword?: string;
}) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
export async function createTcmDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
export async function updateTcmDict(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
export async function deleteTcmDict(data: { type: TcmDictType; ids: number[] }) {
return requestClient.post<any>(`${prefix}delete`, data);
}
/** 手动同步空拼音首拼 */
export async function syncTcmDictPinyin(type: TcmDictType) {
return requestClient.post<any>(`${prefix}sync-pinyin`, { type });
}

View File

@@ -0,0 +1,275 @@
<script lang="ts" setup>
/**
* 中医字典列表(疾病 / 治法 / 证候共用)
* 支持 CRUD + 手动「同步拼音首拼」(仅补空值)
*/
import type { VxeGridListeners } from '#/adapter/vxe-table';
import { computed, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message, Modal as AntModal } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
import {
createTcmDict,
deleteTcmDict,
getTcmDictList,
syncTcmDictPinyin,
updateTcmDict,
type TcmDictType,
} from '../api';
const props = defineProps<{
dictType: TcmDictType;
pageTitle: string;
pageName: string;
}>();
defineOptions({ name: 'TcmDictListPage' });
const hasTopTableDropDownActions = ref(false);
const syncing = ref(false);
const formOptions = {
schema: [
{
component: 'VbenInput',
fieldName: 'keyword',
label: '关键字',
componentProps: { placeholder: '名称/编码/首拼' },
},
],
};
const gridOptions = computed(() => ({
checkboxConfig: { highlight: true, labelField: '' },
columns: [
{ type: 'checkbox', width: 60 },
{ field: 'id', title: 'ID', width: 80 },
{ field: 'code', title: '代码', width: 120 },
{ field: 'number', title: '编号', width: 120 },
{ field: 'name', title: '名称', minWidth: 160 },
{ field: 'alias', title: '别名', minWidth: 120 },
{ field: 'initials_of_pinyin', title: '拼音首拼', width: 120 },
{ field: 'pinyin_full', title: '拼音全拼', minWidth: 160 },
{ field: 'created_at', title: '创建时间', width: 170 },
{ title: '操作', slots: { default: 'action' }, width: 140, fixed: 'right' },
],
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }: any, formValues: any) => {
return await getTcmDictList({
type: props.dictType,
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
height: 'auto',
toolbarConfig: {
search: true,
refresh: true,
slots: { buttons: 'toolbar-buttons' },
},
}));
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
hasTopTableDropDownActions.value =
gridApi.grid.getCheckboxRecords().length > 0;
},
checkboxAll() {
hasTopTableDropDownActions.value =
gridApi.grid.getCheckboxRecords().length > 0;
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridOptions: gridOptions.value as any,
gridEvents,
});
const isUpdate = ref(false);
const [Form, formApi] = useVbenForm({
wrapperClass: 'grid-cols-12',
commonConfig: {
formItemClass: 'col-span-12',
componentProps: { class: 'w-full' },
},
layout: 'horizontal',
schema: [
{
component: 'VbenInput',
fieldName: 'id',
label: 'ID',
dependencies: { show: false, triggerFields: ['id'] },
},
{
component: 'VbenInput',
fieldName: 'name',
label: '名称',
rules: 'required',
},
{
component: 'VbenInput',
fieldName: 'code',
label: '代码',
formItemClass: 'col-span-6',
},
{
component: 'VbenInput',
fieldName: 'number',
label: '编号',
formItemClass: 'col-span-6',
},
{
component: 'VbenInput',
fieldName: 'alias',
label: '别名',
},
{
component: 'VbenInput',
fieldName: 'initials_of_pinyin',
label: '拼音首拼',
componentProps: { placeholder: '保存时按名称自动生成,可留空' },
},
{
component: 'VbenInput',
fieldName: 'pinyin_full',
label: '拼音全拼',
componentProps: { placeholder: '保存时按名称自动生成,可留空' },
},
],
showDefaultActions: false,
});
const [FormModal, formModalApi] = useVbenModal({
class: 'w-[640px]',
draggable: true,
onConfirm: async () => {
const e = await formApi.validate();
if (!e.valid) return;
const values = await formApi.getValues();
formModalApi.setState({ confirmLoading: true });
try {
const payload = { ...values, type: props.dictType };
if (isUpdate.value) {
await updateTcmDict(payload);
} else {
await createTcmDict(payload);
}
message.success('保存成功');
gridApi.query();
formModalApi.close();
} finally {
formModalApi.setState({ confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
formApi.resetFields();
return;
}
const data = formModalApi.getData<{ values?: any; update?: boolean }>();
isUpdate.value = !!data?.update;
if (data?.values && data.update) {
formApi.setValues({ ...data.values });
} else {
formApi.resetFields();
}
},
});
function showModal(row: any = {}, update = false) {
formModalApi.setData({ values: row, update });
formModalApi.open();
}
async function handleDelete(ids: number[]) {
await deleteTcmDict({ type: props.dictType, ids });
message.success('删除成功');
gridApi.query();
}
/** 手动同步:补全空首拼/全拼,并纠正名称含数字但旧拼音丢数字的脏数据 */
function handleSyncPinyin() {
AntModal.confirm({
title: '同步拼音',
content: `将扫描「${props.pageTitle}」中首拼/全拼为空(或含数字名称需纠正)的记录并自动生成。确认执行?`,
onOk: async () => {
syncing.value = true;
try {
const res = await syncTcmDictPinyin(props.dictType);
message.success(
`同步完成:更新 ${Number(res?.updated || 0)} 条,跳过 ${Number(res?.skipped || 0)} 条`,
);
gridApi.query();
} finally {
syncing.value = false;
}
},
});
}
</script>
<template>
<Page auto-content-height :title="pageTitle">
<FormModal :title="`${isUpdate ? '编辑' : '新增'}${pageTitle}`">
<Form />
</FormModal>
<Grid>
<template #toolbar-buttons>
<TableAction
:actions="[
{
label: '新增',
type: 'primary',
onClick: () => showModal({}, false),
},
{
label: '批量删除',
danger: true,
ifShow: () => hasTopTableDropDownActions,
popConfirm: {
title: '确认删除所选?',
confirm: () => {
const ids = gridApi.grid
.getCheckboxRecords()
.map((r: any) => r.id);
handleDelete(ids);
},
},
},
]"
/>
<Button class="ml-2" :loading="syncing" @click="handleSyncPinyin">
同步拼音
</Button>
</template>
<template #action="{ row }">
<TableAction
:actions="[
{ label: '编辑', onClick: () => showModal(row, true) },
{
label: '删除',
danger: true,
popConfirm: {
title: '确认删除?',
confirm: () => handleDelete([row.id]),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
/**
* 中医疾病字典(xk_traditional_chinese_medicine_diseases)
*/
import TcmDictListPage from '../components/TcmDictListPage.vue';
defineOptions({ name: 'TcmDiseasesDict' });
</script>
<template>
<TcmDictListPage
dict-type="diseases"
page-title="中医疾病"
page-name="TcmDiseasesDict"
/>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
/**
* 中医治法字典(xk_traditional_chinese_medicine_method)
*/
import TcmDictListPage from '../components/TcmDictListPage.vue';
defineOptions({ name: 'TcmMethodDict' });
</script>
<template>
<TcmDictListPage
dict-type="method"
page-title="中医治法"
page-name="TcmMethodDict"
/>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
/**
* 中医证候字典(xk_traditional_chinese_medicine_syndrome)
*/
import TcmDictListPage from '../components/TcmDictListPage.vue';
defineOptions({ name: 'TcmSyndromeDict' });
</script>
<template>
<TcmDictListPage
dict-type="syndrome"
page-title="中医证候"
page-name="TcmSyndromeDict"
/>
</template>