cc12458 1 rok temu
rodzic
commit
0be594ee2d

+ 2 - 0
@types/components.d.ts

@@ -44,6 +44,7 @@ declare module 'vue' {
     ATooltip: typeof import('ant-design-vue/es')['Tooltip']
     PatientInfoUpdateReport: typeof import('./../src/components/PatientInfoUpdateReport.vue')['default']
     PatientReportPreview: typeof import('./../src/components/PatientReportPreview.vue')['default']
+    PatientTagEdit: typeof import('./../src/components/PatientTagEdit.vue')['default']
     RecordPatientPreview: typeof import('./../src/components/RecordPatientPreview.vue')['default']
     RecordsAnalysisPreview: typeof import('./../src/components/RecordsAnalysisPreview.vue')['default']
     RecordsIndicatorPreview: typeof import('./../src/components/RecordsIndicatorPreview.vue')['default']
@@ -57,6 +58,7 @@ declare module 'vue' {
     RoleEdit: typeof import('./../src/components/RoleEdit.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    TagEdit: typeof import('./../src/components/TagEdit.vue')['default']
     UserEdit: typeof import('./../src/components/UserEdit.vue')['default']
     UserPassword: typeof import('./../src/components/UserPassword.vue')['default']
     UserPreview: typeof import('./../src/components/UserPreview.vue')['default']

+ 1 - 0
@types/typed-router.d.ts

@@ -22,6 +22,7 @@ declare module 'vue-router/auto-routes' {
     '//patient/history': RouteRecordInfo<'//patient/history', '/patient/history', Record<never, never>, Record<never, never>>,
     '//patient/room': RouteRecordInfo<'//patient/room', '/patient/room', Record<never, never>, Record<never, never>>,
     '//system/role': RouteRecordInfo<'//system/role', '/system/role', Record<never, never>, Record<never, never>>,
+    '//system/tag': RouteRecordInfo<'//system/tag', '/system/tag', Record<never, never>, Record<never, never>>,
     '//system/user': RouteRecordInfo<'//system/user', '/system/user', Record<never, never>, Record<never, never>>,
     '/login': RouteRecordInfo<'/login', '/login', Record<never, never>, Record<never, never>>,
   }

+ 69 - 0
src/components/TagEdit.vue

@@ -0,0 +1,69 @@
+<script setup lang="ts">
+import type { TagModel }                              from '@/model/system.model';
+import { tagEditMethod, tagMethod, tagsSearchMethod } from '@/request/api/system.api';
+import { useRequest }                                 from 'alova/client';
+import { type VxeFormListeners, type VxeFormProps }   from 'vxe-pc-ui';
+
+
+type FormModel = Partial<TagModel>
+
+const defaultModel = {};
+
+const props = defineProps<{ data: FormModel }>();
+const emits = defineEmits<{
+  submit: [ data?: TagModel ],
+}>();
+
+const { loading, send: load } = useRequest(tagMethod, { immediate: false, initialData: props.data ?? defaultModel })
+  .onSuccess(({ data }) => {
+    formProps.data = { ...data };
+  });
+const { loading: submitting, send: submit } = useRequest(tagEditMethod, { immediate: false }).onSuccess(({ data }) => {
+  emits('submit');
+});
+
+const { data: tags, loading: tagsLoading } = useRequest(tagsSearchMethod, { initialData: { total: 0, data: [] } });
+const formProps = reactive<VxeFormProps<FormModel>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: { ...props.data },
+  items: [
+    { field: 'name', title: '标签名称', span: 24, itemRender: { name: 'VxeInput' } },
+    {
+      field: 'parentId', title: '上级标签', span: 24, itemRender: {
+        name: 'VxeSelect',
+        props: {
+          loading: tagsLoading,
+          options: computed(() => tags.value.data),
+          optionProps: { value: 'id', label: 'name' },
+          clearable: true, multiple: true, filterable: true,
+        },
+      },
+    },
+    { align: 'center', span: 24, slots: { default: 'active' } },
+  ],
+  rules: {
+    name: [
+      { required: true, message: '请输入标签名称' },
+    ],
+  },
+});
+const formEmits: VxeFormListeners<FormModel> = {
+  submit({ data }) { submit(data); },
+  reset() { formProps.data = { ...props.data }; },
+};
+
+onBeforeMount(() => {
+  if ( props.data?.tagId ) load(props.data);
+});
+</script>
+<template>
+  <vxe-form v-bind="formProps" v-on="formEmits" :loading>
+    <template #active>
+      <vxe-button type="submit" status="primary" content="提交" :loading="submitting"></vxe-button>
+      <vxe-button type="reset" content="重置" :disabled="submitting"></vxe-button>
+    </template>
+  </vxe-form>
+</template>
+<style scoped lang="scss"></style>

+ 14 - 0
src/model/system.model.ts

@@ -25,3 +25,17 @@ export interface UserModel {
 }
 
 export type UserQuery = Partial<UserModel> & { branch?: string };
+
+export interface TagQuery {
+  tagId: string;
+  name: string;
+  status: '0' | '1';
+}
+
+export interface TagModel {
+  id: string;
+  name: string;
+
+  parentId?: string;
+  parentName?: string;
+}

+ 11 - 8
src/pages/index/patient/history.vue

@@ -1,9 +1,10 @@
 <script setup lang="ts">
 import ReportHistoryPreview                      from '@/components/ReportHistoryPreview.vue';
 import type { PatientQuery, PatientReportModel } from '@/model';
-import { patientsHistoryMethod }                 from '@/request/api/patient.api';
-import { usePagination }                         from 'alova/client';
-import { h }                                     from 'vue';
+import { patientsHistoryMethod }     from '@/request/api/patient.api';
+import { tagsSearchMethod }                      from '@/request/api/system.api';
+import { usePagination, useRequest } from 'alova/client';
+import { h }                         from 'vue';
 
 import {
   VxeButton,
@@ -15,6 +16,7 @@ import {
   VxeUI,
 } from 'vxe-pc-ui';
 
+const { data: tags, loading: tagsLoading } = useRequest(tagsSearchMethod, { initialData: { total: 0, data: [] } });
 
 const model = shallowRef<PatientQuery>();
 const searchFormProps = reactive<VxeFormProps<PatientQuery>>({
@@ -39,12 +41,13 @@ const searchFormProps = reactive<VxeFormProps<PatientQuery>>({
       },
     },
     {
-      field: 'tags', title: '标签', span: 6, itemRender: {
-        name: 'VxeTreeSelect',
+      field: 'tagIds', title: '上级标签', span: 6, itemRender: {
+        name: 'VxeSelect',
         props: {
-          loading: computed(() => true),
-          options: computed(() => []),
-          optionProps: { value: 'id' },
+          loading: tagsLoading,
+          options: computed(() => tags.value.data),
+          optionProps: { value: 'id', label: 'name' },
+          clearable: true, multiple: true, filterable: true,
         },
       },
     },

+ 8 - 3
src/pages/index/patient/room@default.vue

@@ -5,8 +5,10 @@ import type { ReportModel }                                from '@/model';
 import { patientMethod }                                   from '@/request/api/patient.api';
 import { reportMethod, reportSchemeMethod, reportsMethod } from '@/request/api/report.api';
 import PatientCardWidget                                   from '@/widgets/PatientCardWidget.vue';
+import PatientTagWidget                                    from '@/widgets/PatientTagWidget.vue';
 import ReportCardWidget                                    from '@/widgets/ReportCardWidget.vue';
 import ReportSchemeCardWidget                              from '@/widgets/ReportSchemeCardWidget.vue';
+import { useElementSize }                                  from '@vueuse/core';
 import { useRouteQuery }                                   from '@vueuse/router';
 import { invalidateCache }                                 from 'alova';
 import { useWatcher }                                      from 'alova/client';
@@ -131,12 +133,15 @@ function openAnalysisEditHandle() {
     },
   });
 }
+
+const patientCardRef = ref<HTMLElement>(null);
+const { height } = useElementSize(patientCardRef);
 </script>
 <template>
   <div id="page-container-scroller" class="page-container flex flex-row">
     <div class="page-container-main flex-auto flex flex-col">
       <div class="area">
-        <PatientCardWidget :dataset="patient" :loading="patientLoading" />
+        <PatientCardWidget ref="patientCardRef" :dataset="patient" :loading="patientLoading" />
         <ReportCardWidget :dataset="report" :loading="reportLoading" collapsible :collapsed="true">
           <template #analysis>
             <a-button
@@ -144,7 +149,7 @@ function openAnalysisEditHandle() {
               :loading="analysisReportPreviewOpening"
               @click="analysisReportPreviewOpening = true;openAnalysisEditHandle()"
             >
-              信息采集
+              信息采集-{{ height }}
             </a-button>
           </template>
         </ReportCardWidget>
@@ -169,7 +174,7 @@ function openAnalysisEditHandle() {
     </div>
     <div class="page-container-aside flex-none flex flex-col">
       <div class="area card flex-auto flex flex-col">
-        <div class="flex-none" style="height: 20%;"></div>
+        <PatientTagWidget :style="{height: `${height}px`}" :patient="patient" editable />
         <div class="card__header flex justify-between items-center">
           <div class="card__title flex-none">
             <span>报告记录</span>

+ 3 - 3
src/pages/index/system/role.vue

@@ -138,14 +138,14 @@ onMounted(() => {
 });
 
 function updateRoleStatus(model: RoleModel, index: number, status: RoleModel['status']) {
-  const { roleName } = model;
+  const { roleId, roleName } = model;
   const label = { '1': '停用', '0': '启用' }[ status ];
   VxeUI.modal.confirm({
     title: `启用状态`,
     content: `确认要 ${ label } ${ roleName } 角色吗?`,
     showClose: false,
     onConfirm() {
-      updateRoleStatusMethod(model).then(() => {
+      updateRoleStatusMethod({ roleId, status }).then(() => {
         notification.success({
           message: `${ label }角色: ${ roleName }`,
           description: '操作成功',
@@ -161,7 +161,7 @@ function deleteRole(model: RoleModel, index: number) {
   const { roleName } = model;
   VxeUI.modal.confirm({
     title: `删除角色`,
-    content: `确认要删除 ${ roleName }角色吗?`,
+    content: `确认要删除 ${ roleName } 角色吗?`,
     showClose: false,
     onConfirm() {
       deleteRoleMethod(model).then(() => {

+ 242 - 0
src/pages/index/system/tag.vue

@@ -0,0 +1,242 @@
+<script setup lang="ts">
+import TagEdit           from '@/components/TagEdit.vue';
+import { statusOptions } from '@/model/options';
+
+import type { TagModel, TagQuery } from '@/model/system.model';
+
+import { tagDeleteMethod, tagsMethod, tagsSearchMethod, tagUpdateStatusMethod } from '@/request/api/system.api';
+import { usePagination, useRequest }                                            from 'alova/client';
+import { notification }                                                         from 'ant-design-vue';
+
+import {
+  type VxeFormListeners,
+  type VxeFormProps,
+  type VxeGridInstance,
+  type VxeGridListeners,
+  type VxeGridProps,
+  VxeUI,
+} from 'vxe-pc-ui';
+
+
+const { data: tags, loading: tagsLoading } = useRequest(tagsSearchMethod, { initialData: { total: 0, data: [] } });
+
+const model = shallowRef<TagQuery>();
+const searchFormProps = reactive<VxeFormProps<TagQuery>>({
+  titleWidth: 100,
+  titleAlign: 'right',
+  titleColon: true,
+  data: {
+    name: '',
+    status: void 0,
+  },
+  items: [
+    { field: 'name', title: '标签名称', span: 6, itemRender: { name: 'VxeInput' } },
+    {
+      field: 'status', title: '是否启用', span: 6, itemRender: {
+        name: 'VxeSelect',
+        options: statusOptions,
+      },
+    },
+    { field: 'status', title: '创建者', span: 6, itemRender: { name: 'VxeInput' } },
+    {
+      field: 'parentId', title: '上级标签', span: 6, itemRender: {
+        name: 'VxeSelect',
+        props: {
+          loading: tagsLoading,
+          options: computed(() => tags.value.data),
+          optionProps: { value: 'id', label: 'name' },
+          clearable: true, multiple: true, filterable: true,
+        },
+      },
+    },
+    {
+      span: 6, itemRender: {
+        name: 'VxeButtonGroup',
+        options: [
+          { type: 'submit', content: '查询', status: 'primary' },
+          { type: 'reset', content: '清空' },
+        ],
+      },
+    },
+  ],
+});
+const searchFormEmits: VxeFormListeners<TagQuery> = {
+  submit({ data }) { model.value = { ...data }; },
+  reset({ data }) { model.value = { ...data }; },
+};
+
+const gridRef = ref<VxeGridInstance<TagModel>>();
+const gridOptions = reactive<VxeGridProps<TagModel>>({
+  id: 'tag-list',
+  border: true,
+  showOverflow: true,
+  height: 'auto',
+  scrollY: { enabled: true, gt: 0 },
+  toolbarConfig: {
+    custom: true,
+    zoom: true,
+    slots: {
+      buttons: 'handle',
+      tools: 'toolbar-extra',
+    },
+  },
+  columnConfig: {
+    resizable: true,
+  },
+  customConfig: {
+    storage: true,
+  },
+  columns: [
+    { type: 'seq', width: 70, fixed: 'left' },
+    { field: 'name', title: '标签名称', minWidth: 160 },
+    { field: 'parentName', title: '上级标签', minWidth: 160 },
+    { field: 'update_time', title: '最近一次修改时间', minWidth: 160 },
+    { field: 'user_name', title: '创建者', minWidth: 160 },
+    {
+      field: 'status', title: '启用状态', minWidth: 100, cellRender: {
+        name: 'VxeSwitch',
+        props: {
+          openLabel: '启用', openValue: '0',
+          closeLabel: '停用', closeValue: '1',
+        },
+        events: {
+          change({ row, rowIndex }, { value }) {
+            row.status = { '1': '0', '0': '1' }[ value as string ] as any;
+            updateTagStatus(row, rowIndex, value);
+          },
+        },
+      },
+    },
+    {
+      title: '操作',
+      width: 200,
+      cellRender: {
+        name: 'VxeButtonGroup',
+        props: {
+          mode: 'text',
+        },
+        options: [
+          { content: '修改', name: 'editTag' },
+          { content: '删除', status: 'error', name: 'deleteTag' },
+        ],
+        events: {
+          click({ row, rowIndex }, { name }) {
+            let method;
+            if ( name === 'editTag' ) { method = editTag; } else if ( name === 'deleteTag' ) { method = deleteTag; }
+            method?.(row, rowIndex);
+          },
+        },
+      },
+    },
+  ],
+  data: [],
+});
+const gridEvents: VxeGridListeners = {};
+
+const { loading, page, pageSize, total, onSuccess, replace, refresh, remove } = usePagination(
+  (page, size) => tagsMethod(page, size, model.value), {
+    initialData: { data: [], total: 0 },
+    initialPage: 1,
+    initialPageSize: 100,
+    watchingStates: [ model ],
+    immediate: false,
+  },
+);
+onSuccess(({ data: { data } }) => {
+  gridRef.value?.loadData(data);
+});
+
+onMounted(() => {
+  model.value = toRaw(searchFormProps.data);
+});
+
+function updateTagStatus(model: TagModel, index: number, status: TagModel['status']) {
+  const { id, name } = model;
+  const label = { '1': '停用', '0': '启用' }[ status ];
+  VxeUI.modal.confirm({
+    title: `启用状态`,
+    content: `确认要 ${ label } ${ name } 标签吗?`,
+    showClose: false,
+    onConfirm() {
+      tagUpdateStatusMethod({ id, status }).then(() => {
+        notification.success({
+          message: `${ label }标签: ${ name }`,
+          description: '操作成功',
+        });
+        model.status = status;
+        replace(model, index);
+      });
+    },
+  });
+}
+
+function deleteTag(model: TagModel, index: number) {
+  const { name } = model;
+  VxeUI.modal.confirm({
+    title: `删除标签`,
+    content: `确认要删除 ${ name } 标签吗?`,
+    showClose: false,
+    onConfirm() {
+      tagDeleteMethod(model).then(() => {
+        notification.success({
+          message: `删除标签: ${ name }`,
+          description: '操作成功',
+        });
+        refresh(page.value);
+      });
+    },
+  });
+}
+
+function editTag(model?: TagModel, index?: number) {
+  VxeUI.modal.open({
+    title: model?.id ? `修改标签` : `新增标签`,
+    escClosable: true,
+    destroyOnClose: true,
+    id: `tag-edit-modal`,
+    remember: true,
+    storage: true,
+    slots: {
+      default() {
+        return h(TagEdit, <any> {
+          data: model, onSubmit(data: TagModel) {
+            refresh(page.value);
+            VxeUI.modal.close(`tag-edit-modal`);
+          },
+        });
+      },
+    },
+  });
+}
+</script>
+<template>
+  <div class="page-container flex flex-col">
+    <header class="flex-none">
+      <vxe-form v-bind="searchFormProps" v-on="searchFormEmits"></vxe-form>
+    </header>
+    <main class="flex-auto overflow-hidden">
+      <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents" :loading="loading">
+        <template #handle>
+          <vxe-button status="primary" @click="editTag()">新增</vxe-button>
+        </template>
+        <template #toolbar-extra>
+          <vxe-button style="margin-right: 12px;" icon="vxe-icon-repeat" circle @click="refresh(page)"></vxe-button>
+        </template>
+      </vxe-grid>
+    </main>
+    <footer class="flex-none">
+      <vxe-pager
+        v-model:current-page="page"
+        v-model:page-size="pageSize"
+        :total="total"
+        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']"
+      />
+    </footer>
+  </div>
+</template>
+<style scoped lang="scss">
+.page-container {
+  padding: 0 24px;
+  max-height: var(--page-main-container);
+}
+</style>

+ 7 - 0
src/request/api/patient.api.ts

@@ -44,3 +44,10 @@ export function patientMethod(id: string) {
     },
   });
 }
+
+export function patientMark(id: string, data: string[]) {
+  return request.Get(`/fdhb-pc/patientInfoManage/updatePatientTag`, {
+    params: { patientId: id, tagIds: data },
+    cacheFor: null,
+  });
+}

+ 42 - 2
src/request/api/system.api.ts

@@ -1,5 +1,5 @@
-import type { List, Tree }                                 from '@/model';
-import type { RoleModel, RoleQuery, UserModel, UserQuery } from '@/model/system.model';
+import type { List, Tree }                                                     from '@/model';
+import type { RoleModel, RoleQuery, TagModel, TagQuery, UserModel, UserQuery } from '@/model/system.model';
 
 import request from '@/request/alova';
 
@@ -57,3 +57,43 @@ export function deleteRoleMethod(data: Partial<RoleModel>) {
 export function updateRoleStatusMethod(data: Partial<RoleModel>) {
   return request.Put(`/prod-api/system/role/changeStatus`, { roleId: data.roleId, status: data.status });
 }
+
+export function tagsMethod(page: number, size: number, query?: TagQuery) {
+  return request.Post<List<TagModel>>(`/fdhb-pc/tagManage/pageTag`, query ?? {}, {
+    params: { pageNum: page, pageSize: size },
+  });
+}
+
+export function tagsSearchMethod(query?: TagQuery) {
+  return request.Post<List<TagModel>, TagModel[]>(`/fdhb-pc/tagManage/selectTag`, query ?? {}, {
+    transform(data) {
+      return { total: data.length, data };
+    },
+  });
+}
+
+export function tagMethod(data: Partial<TagModel>) {
+  return request.Get<TagModel>(`/prod-api/system/role/${ data.roleId }`);
+}
+
+
+export function tagEditMethod(data: Partial<TagModel>) {
+  return data.id
+         ? request.Post(`/fdhb-pc/tagManage/updateTag`, { ...data, tagId: data.id })
+         : request.Post(`/fdhb-pc/tagManage/addTag`, data);
+}
+
+export function tagDeleteMethod(data: Partial<TagModel>) {
+  return request.Get(`/fdhb-pc/tagManage/deleteTag`, {
+    params: { tagId: data.id },
+    cacheFor: null,
+  });
+}
+
+
+export function tagUpdateStatusMethod(data: Partial<TagModel>) {
+  return request.Get(`/fdhb-pc/tagManage/updateStatus`, {
+    params: { tagId: data.id, status: data.status },
+    cacheFor: null,
+  });
+}

+ 39 - 0
src/widgets/PatientTagWidget.vue

@@ -0,0 +1,39 @@
+<script setup lang="ts">
+import type { PatientModel } from '@/model';
+import { patientMark }       from '@/request/api/patient.api';
+import { tagsSearchMethod }  from '@/request/api/system.api';
+import { useRequest }        from 'alova/client';
+
+
+const { data: tags, loading: tagsLoading } = useRequest(tagsSearchMethod, { initialData: { total: 0, data: [] } });
+const { send } = useRequest((data: string[]) => patientMark(props.patient.id, data), { immediate: false });
+
+const props = defineProps<{
+  patient: Partial<PatientModel>;
+
+  editable?: boolean;
+}>();
+
+function save({ value }) {
+  console.log(value);
+  send(value);
+}
+</script>
+<template>
+  <div class="card">
+    <div class="card__header sticky flex justify-between items-center">
+      <div class="card__title">
+        <span>标签</span>
+        <a-spin v-if="props.loading" size="small" style="margin-left: 4px;" />
+      </div>
+    </div>
+    <div class="card__content">
+      <vxe-select :options="tags.data" :loading="tagsLoading" :option-props="{value: 'id', label: 'name'}"
+                  @change="save" multiple
+      ></vxe-select>
+    </div>
+  </div>
+</template>
+<style scoped lang="scss">
+@import "@/themes/report-card";
+</style>