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>

View File

@@ -0,0 +1,36 @@
<script lang="ts" setup>
/**
* 病历公共词条-按字段拆分页(从路由 path 末段解析 field_code)
* 例:/system/medical-record/entry-public/chief-complaint → chief_complaint
*/
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import EntryListPage from '#/views/doctor/medical-record/components/EntryListPage.vue';
import { MEDICAL_RECORD_FIELD_OPTIONS } from '#/views/doctor/medical-record/config/constants';
defineOptions({ name: 'MedicalRecordEntryPublicField' });
const route = useRoute();
const fieldCode = computed(() => {
const seg = (route.path.split('/').filter(Boolean).pop() || '').trim();
return seg.replace(/-/g, '_');
});
const fieldLabel = computed(() => {
const hit = MEDICAL_RECORD_FIELD_OPTIONS.find((o) => o.value === fieldCode.value);
return hit?.label || fieldCode.value;
});
const pageTitle = computed(() => `公共词条·${fieldLabel.value}`);
</script>
<template>
<EntryListPage
:key="fieldCode"
scope="public"
:fixed-field-code="fieldCode"
:page-title="pageTitle"
:page-name="String(route.name || 'MedicalRecordEntryPublicField')"
/>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
/**
* 病历公共词条管理(平台)
*/
import EntryListPage from '#/views/doctor/medical-record/components/EntryListPage.vue';
defineOptions({ name: 'MedicalRecordEntryPublic' });
</script>
<template>
<EntryListPage
scope="public"
page-title="病历公共词条"
page-name="MedicalRecordEntryPublic"
/>
</template>

View File

@@ -15,3 +15,10 @@ export async function getPriceAdjustConfig(storeId?: number) {
params: storeId ? { store_id: storeId } : {},
});
}
/** 按 key 批量读取系统配置(如 dict_search_rank_mode) */
export async function getSystemConfigByKeys(keys: string[]) {
return requestClient.get<any>(`${prefix}get-by-keys`, {
params: { keys: keys.join(',') },
});
}

View File

@@ -40,6 +40,13 @@ const logisticsShowWhNameClinic = ref(true);
const salespersonStoreEditEnabled = ref(false);
/** 业务端单机登录:医生/药师/诊所管理员等同一账号仅 1 手机 + 1 网页(默认否) */
const doctorSingleDeviceLogin = ref(false);
/** 字典搜索匹配度:backend 服务端 / frontend 前端 */
const dictSearchRankMode = ref<'backend' | 'frontend'>('backend');
/**
* 病历诊断/医嘱「常用|我的」展示位置
* tags=文本域下标签(默认) bubble=气泡内展示 both=两边
*/
const mrCommonDisplayMode = ref<'tags' | 'bubble' | 'both'>('tags');
/** 开票通知渠道:subscribe / sms,默认仅订阅消息 */
const invoiceNoticeChannels = ref<string[]>(['subscribe']);
@@ -53,6 +60,8 @@ function readStoredTab() {
stored === 'salesperson' ||
stored === 'invoice_notice' ||
stored === 'doctor_login' ||
stored === 'dict_search' ||
stored === 'medical_record' ||
stored === 'oa_notify'
) {
activeKey.value = stored;
@@ -121,6 +130,15 @@ async function load() {
if (row.config_key === 'doctor_single_device_login') {
doctorSingleDeviceLogin.value = parseBoolConfig(row.config_value, false);
}
if (row.config_key === 'dict_search_rank_mode') {
dictSearchRankMode.value =
String(row.config_value || '') === 'frontend' ? 'frontend' : 'backend';
}
if (row.config_key === 'mr_common_display_mode') {
const mode = String(row.config_value || 'tags');
mrCommonDisplayMode.value =
mode === 'bubble' || mode === 'both' ? mode : 'tags';
}
if (row.config_key === 'invoice_notice_channels') {
let channels: unknown = row.config_value;
if (typeof channels === 'string') {
@@ -202,6 +220,22 @@ async function handleSave() {
description: '业务端单机登录(医生/药师/诊所管理员等:1手机+1网页)',
sort: 310,
},
{
config_key: 'dict_search_rank_mode',
config_value: dictSearchRankMode.value,
value_type: 'string',
config_group: 'search',
description: '字典搜索匹配度:backend服务端 / frontend前端',
sort: 320,
},
{
config_key: 'mr_common_display_mode',
config_value: mrCommonDisplayMode.value,
value_type: 'string',
config_group: 'medical_record',
description: '病历诊断医嘱常用展示:tags标签 / bubble气泡 / both两边',
sort: 330,
},
{
config_key: 'invoice_notice_channels',
config_value: JSON.stringify(
@@ -331,6 +365,33 @@ onMounted(() => {
</div>
</Tabs.TabPane>
<Tabs.TabPane key="dict_search" tab="字典搜索">
<div class="py-4">
<div class="mb-2 font-medium">诊断 / 医嘱 / 病历词条匹配度处理端</div>
<div class="mb-2 text-sm text-gray-500">
后端:服务端计算匹配度并排序(适合疾病库等大数据量);前端:接口只返回候选,由端上打分排序(适合调试或减轻服务端 CPU)。关键字高亮始终在端上渲染。
</div>
<Radio.Group v-model:value="dictSearchRankMode">
<Radio value="backend">后端处理(推荐)</Radio>
<Radio value="frontend">前端处理</Radio>
</Radio.Group>
</div>
</Tabs.TabPane>
<Tabs.TabPane key="medical_record" tab="词条">
<div class="py-4">
<div class="mb-2 font-medium">诊断 / 医嘱「常用|我的」展示位置</div>
<div class="mb-2 text-sm text-gray-500">
控制接诊病历里诊断、医嘱的常用快捷出现在哪里。默认「标签」即文本域下方的快捷标签;「气泡卡片」则在关键字搜索气泡内默认展示常用;「两边」同时展示。
</div>
<Radio.Group v-model:value="mrCommonDisplayMode">
<Radio value="tags">标签(默认)</Radio>
<Radio value="bubble">气泡卡片默认展示</Radio>
<Radio value="both">两边都展示</Radio>
</Radio.Group>
</div>
</Tabs.TabPane>
<Tabs.TabPane key="invoice_notice" tab="开票通知">
<div class="py-4">
<div class="mb-2 font-medium">患者开票通知渠道</div>

View File

@@ -0,0 +1,24 @@
import { requestClient } from '#/api/request';
/** VIP 功能字典 API */
const prefix = 'vip-feature/';
export async function getVipFeatureList(data: any) {
return requestClient.get<any>(`${prefix}list`, { params: data });
}
export async function getVipFeatureOption(data?: any) {
return requestClient.get<any>(`${prefix}option`, { params: data });
}
export async function createVipFeature(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}create`, data);
}
export async function updateVipFeature(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}update`, data);
}
export async function deleteVipFeature(data: Record<string, any>) {
return requestClient.post<any>(`${prefix}delete`, data);
}

View File

@@ -0,0 +1,61 @@
<script lang="ts" setup>
/**
* VIP 功能字典新增/编辑弹窗
*/
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createVipFeature, updateVipFeature } from '../api';
import { modalFormProps } from '../config/form';
const isUpdate = ref(false);
const gridApi = ref();
const [Form, formApi] = useVbenForm(modalFormProps);
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
const e = await formApi.validate();
if (!e.valid) return;
const values = await formApi.getValues();
modalApi.setState({ confirmLoading: true });
try {
const api = isUpdate.value ? updateVipFeature : createVipFeature;
await api(values);
message.success('保存成功');
gridApi.value?.query();
modalApi.close();
} finally {
modalApi.setState({ confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (!isOpen) {
formApi.resetFields();
return;
}
const { values, update } = modalApi.getData<Record<string, any>>() || {};
isUpdate.value = !!update;
if (values && update) {
formApi.setValues({ ...values });
} else {
formApi.resetFields();
formApi.setValues({ sort: 0, status: 1, description: '' });
}
},
});
</script>
<template>
<Modal :title="`${isUpdate ? '编辑' : '新增'}VIP功能`" class="w-[560px]">
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,5 @@
/** VIP 功能字典常量 */
export const STATUS_OPTIONS = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
];

View File

@@ -0,0 +1,59 @@
import type { VbenFormProps } from '#/adapter/form';
import { STATUS_OPTIONS } from './constants';
/** VIP 功能字典表单 */
export const modalFormProps: VbenFormProps = {
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: 'code',
label: '功能编码',
rules: 'required',
componentProps: { placeholder: 'snake_case,如 medical_record' },
},
{
component: 'VbenInput',
fieldName: 'name',
label: '功能名称',
rules: 'required',
componentProps: { placeholder: '如:病历' },
},
{
component: 'VbenInput',
fieldName: 'description',
label: '说明',
componentProps: { type: 'textarea', rows: 2 },
defaultValue: '',
},
{
component: 'InputNumber',
fieldName: 'sort',
label: '排序',
formItemClass: 'col-span-6',
componentProps: { min: 0 },
defaultValue: 0,
},
{
component: 'VbenSelect',
fieldName: 'status',
label: '状态',
formItemClass: 'col-span-6',
componentProps: { options: STATUS_OPTIONS },
defaultValue: 1,
},
],
showDefaultActions: false,
};

View File

@@ -0,0 +1,169 @@
<script lang="ts" setup>
/**
* VIP 功能字典列表:供等级 permissions 勾选的功能码维护
*/
import { onMounted, reactive, ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import {
Button,
Card,
Empty,
Input,
Pagination,
Select,
Tag,
message,
} from 'ant-design-vue';
import { TableAction } from '#/components/table-action';
import { deleteVipFeature, getVipFeatureList } from './api';
import FormModal from './components/modal.vue';
import { STATUS_OPTIONS } from './config/constants';
defineOptions({ name: 'VipFeature' });
const loading = ref(false);
const list = ref<any[]>([]);
const total = ref(0);
const page = reactive({ current: 1, pageSize: 12 });
const filters = reactive({
code: '',
name: '',
status: undefined as number | undefined,
});
const [FormModalComp, formModalApi] = useVbenModal({
connectedComponent: FormModal,
});
async function loadList() {
loading.value = true;
try {
const res = await getVipFeatureList({
page: page.current,
pageSize: page.pageSize,
code: filters.code || undefined,
name: filters.name || undefined,
status: filters.status,
});
list.value = res?.items || [];
total.value = Number(res?.total || 0);
} finally {
loading.value = false;
}
}
function onSearch() {
page.current = 1;
loadList();
}
function onReset() {
filters.code = '';
filters.name = '';
filters.status = undefined;
onSearch();
}
const showModal = (data: any = {}, isUpdate = false) => {
formModalApi.setData({
values: data,
update: isUpdate,
gridApi: { query: loadList, reload: loadList },
});
formModalApi.open();
};
const handleDelete = (id: number) => {
deleteVipFeature({ ids: [id] }).then(() => {
message.success('删除成功');
loadList();
});
};
onMounted(loadList);
</script>
<template>
<Page auto-content-height title="VIP功能字典">
<FormModalComp />
<div class="p-4">
<div class="mb-4 flex flex-wrap items-center gap-3">
<Input
v-model:value="filters.code"
allow-clear
placeholder="功能编码"
class="w-40"
@press-enter="onSearch"
/>
<Input
v-model:value="filters.name"
allow-clear
placeholder="功能名称"
class="w-40"
@press-enter="onSearch"
/>
<Select
v-model:value="filters.status"
allow-clear
placeholder="状态"
class="w-28"
:options="STATUS_OPTIONS"
/>
<Button type="primary" @click="onSearch">查询</Button>
<Button @click="onReset">重置</Button>
<TableAction
:actions="[
{
label: '新增功能',
type: 'primary',
onClick: () => showModal({}, false),
},
]"
/>
</div>
<div v-if="list.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<Card v-for="item in list" :key="item.id" size="small" :loading="loading">
<template #title>
<span class="font-medium">{{ item.name }}</span>
<Tag class="ml-2" :color="item.status === 1 ? 'success' : 'default'">
{{ item.status_txt }}
</Tag>
</template>
<template #extra>
<TableAction
:actions="[
{ label: '编辑', onClick: () => showModal(item, true) },
{
label: '删除',
danger: true,
popConfirm: {
title: '确认删除该功能?',
confirm: () => handleDelete(item.id),
},
},
]"
/>
</template>
<div class="text-sm text-gray-600">
<div>编码:{{ item.code }}</div>
<div class="mt-1">说明:{{ item.description || '-' }}</div>
<div class="mt-1">排序:{{ item.sort }}</div>
</div>
</Card>
</div>
<Empty v-else description="暂无功能,请先新增或执行 SQL 种子" />
<div class="mt-4 flex justify-end">
<Pagination
v-model:current="page.current"
v-model:page-size="page.pageSize"
:total="total"
show-size-changer
@change="loadList"
/>
</div>
</div>
</Page>
</template>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
/**
* VIP 等级新增/编辑弹窗
* permissions 从 VIP 功能字典 option 多选
*/
import { ref } from 'vue';
@@ -9,6 +10,7 @@ import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getVipFeatureOption } from '#/views/system/vip/feature/api';
import { createVipLevel, updateVipLevel } from '../api';
import { modalFormProps } from '../config/form';
@@ -37,12 +39,30 @@ const [Modal, modalApi] = useVbenModal({
modalApi.setState({ confirmLoading: false });
}
},
onOpenChange(isOpen: boolean) {
async onOpenChange(isOpen: boolean) {
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
if (!isOpen) {
formApi.resetFields();
return;
}
// 打开时拉取功能字典,注入 permissions 下拉
try {
const opts = await getVipFeatureOption();
formApi.updateSchema([
{
fieldName: 'permissions',
componentProps: {
mode: 'multiple',
allowClear: true,
optionFilterProp: 'label',
placeholder: '从功能字典多选',
options: Array.isArray(opts) ? opts : [],
},
},
]);
} catch (e) {
console.error('加载 VIP 功能字典失败', e);
}
const { values, update } = modalApi.getData<Record<string, any>>() || {};
isUpdate.value = !!update;
if (values && update) {

View File

@@ -72,11 +72,14 @@ export const modalFormProps: VbenFormProps = {
{
component: 'Select',
fieldName: 'permissions',
label: '权限码',
label: 'VIP功能',
help: '低等级勾选后,更高权重等级会自动补齐缺失功能',
componentProps: {
mode: 'tags',
placeholder: '输入权限码后回车,预留业务权益',
tokenSeparators: [','],
mode: 'multiple',
allowClear: true,
optionFilterProp: 'label',
placeholder: '从功能字典多选(需先在 VIP功能字典 维护)',
options: [],
},
defaultValue: [],
},