Explorar o código

feat(@six/smart-pharmacy): 智慧药事系统第二版点评专家静态页面新增

cmj hai 1 mes
pai
achega
2a1d667eba

+ 22 - 0
apps/smart-pharmacy/public/database/menu.json

@@ -92,5 +92,27 @@
         "component": "/prescription/management/detail"
       }
     ]
+  },
+  {
+    "id": "2500",
+    "meta": {
+      "icon": "mdi:clipboard-text-search-outline",
+      "order": 9996,
+      "title": "处方点评"
+    },
+    "name": "PrescriptionReview",
+    "path": "/prescription-review",
+    "children": [
+      {
+        "id": "2501",
+        "path": "/prescription-review/expert",
+        "name": "PrescriptionReviewExpert",
+        "meta": {
+          "icon": "mdi:account-tie",
+          "title": "点评专家"
+        },
+        "component": "/prescription-review/expert/list"
+      }
+    ]
   }
 ]

+ 1 - 0
apps/smart-pharmacy/src/api/index.ts

@@ -17,6 +17,7 @@ export * from './method/business';
 export * from './method/common';
 export * from './method/dict';
 export * from './method/prescription';
+export * from './method/prescription-review';
 export * from './method/system';
 
 export const http = createRequestClient({

+ 5 - 2
apps/smart-pharmacy/src/api/method/access.ts

@@ -5,8 +5,10 @@ import type { SystemModel, TransformData } from '#/api';
 import { http } from '#/api';
 import { fromRole } from '#/api/model';
 import {
+  ensureHardcodedMenusVisible,
   filterMenusByPermissions,
   fromTreeSelectMenus,
+  mergeHardcodedMenuTree,
   type TreeSelectMenuNode,
 } from '#/api/model/menu';
 
@@ -36,9 +38,10 @@ export function getAccessMenuMethod(permissions?: string[]) {
     {
       cacheFor: 0,
       transform(data) {
-        const menus = fromTreeSelectMenus(data);
+        const menus = fromTreeSelectMenus(mergeHardcodedMenuTree(data));
         if (!permissions?.length) return menus;
-        return filterMenusByPermissions(permissions, menus);
+        const filtered = filterMenusByPermissions(permissions, menus);
+        return ensureHardcodedMenusVisible(filtered, menus);
       },
     },
   );

+ 168 - 0
apps/smart-pharmacy/src/api/method/prescription-review.ts

@@ -0,0 +1,168 @@
+import type { TransformList, TransformRecord } from '#/api';
+
+/** 点评专家(接口就绪后替换为真实请求) */
+export namespace PrescriptionReviewModel {
+  export interface ReviewExpert extends TransformRecord {
+    access: string;
+    name: string;
+    hospitalName: string;
+    phone: string;
+    /** 0 启用,1 禁用 */
+    status: 0 | 1;
+    institutionId?: string;
+    pid?: string;
+  }
+}
+
+const MOCK_EXPERTS: PrescriptionReviewModel.ReviewExpert[] = [
+  {
+    id: '1',
+    access: 'za00011',
+    name: '孙明1',
+    hospitalName: '蒋村社区卫生服务中心',
+    phone: '13112342344',
+    status: 1,
+    institutionId: 'org-1',
+    createUser: '张三',
+    createTime: '2023-05-15 13:35',
+  },
+  {
+    id: '2',
+    access: 'za00012',
+    name: '孙明2',
+    hospitalName: '蒋村社区卫生服务中心',
+    phone: '13156782344',
+    status: 0,
+    institutionId: 'org-1',
+    createUser: '张三',
+    createTime: '2023-05-16 09:20',
+  },
+  {
+    id: '3',
+    access: 'za00013',
+    name: '李华',
+    hospitalName: '西溪社区卫生服务中心',
+    phone: '13298765432',
+    status: 0,
+    institutionId: 'org-2',
+    createUser: '李四',
+    createTime: '2023-06-01 10:15',
+  },
+  {
+    id: '4',
+    access: 'za00014',
+    name: '王芳',
+    hospitalName: '西溪社区卫生服务中心',
+    phone: '13387654321',
+    status: 1,
+    institutionId: 'org-2',
+    createUser: '李四',
+    createTime: '2023-06-02 14:40',
+  },
+  {
+    id: '5',
+    access: 'za00015',
+    name: '赵强',
+    hospitalName: '蒋村社区卫生服务中心',
+    phone: '13567891234',
+    status: 0,
+    institutionId: 'org-1',
+    createUser: '王五',
+    createTime: '2023-07-10 08:50',
+  },
+  {
+    id: '6',
+    access: 'za00016',
+    name: '周敏',
+    hospitalName: '文新社区卫生服务中心',
+    phone: '13678901234',
+    status: 1,
+    institutionId: 'org-3',
+    createUser: '王五',
+    createTime: '2023-07-11 16:25',
+  },
+  {
+    id: '7',
+    access: 'za00017',
+    name: '吴磊',
+    hospitalName: '文新社区卫生服务中心',
+    phone: '13789012345',
+    status: 0,
+    institutionId: 'org-3',
+    createUser: '赵六',
+    createTime: '2023-08-05 11:30',
+  },
+  {
+    id: '8',
+    access: 'za00018',
+    name: '郑洁',
+    hospitalName: '蒋村社区卫生服务中心',
+    phone: '13890123456',
+    status: 0,
+    institutionId: 'org-1',
+    createUser: '赵六',
+    createTime: '2023-08-06 15:45',
+  },
+  {
+    id: '9',
+    access: 'za00019',
+    name: '钱伟',
+    hospitalName: '西溪社区卫生服务中心',
+    phone: '13901234567',
+    status: 1,
+    institutionId: 'org-2',
+    createUser: '张三',
+    createTime: '2023-09-12 09:10',
+  },
+  {
+    id: '10',
+    access: 'za00020',
+    name: '孙丽',
+    hospitalName: '文新社区卫生服务中心',
+    phone: '13012345678',
+    status: 0,
+    institutionId: 'org-3',
+    createUser: '李四',
+    createTime: '2023-09-13 13:55',
+  },
+];
+
+function filterExperts(
+  items: PrescriptionReviewModel.ReviewExpert[],
+  query?: Partial<PrescriptionReviewModel.ReviewExpert>,
+) {
+  const institutionId = query?.institutionId;
+  if (!institutionId) return items;
+  return items.filter((item) => item.institutionId === String(institutionId));
+}
+
+/** 点评专家列表(当前为本地 mock,后期对接后端分页接口) */
+export function listReviewExpertsMethod(
+  page = 1,
+  size = 10,
+  query?: Partial<PrescriptionReviewModel.ReviewExpert>,
+): Promise<TransformList<PrescriptionReviewModel.ReviewExpert>> {
+  const filtered = filterExperts(MOCK_EXPERTS, query);
+  const start = (page - 1) * size;
+  const items = filtered.slice(start, start + size);
+  return Promise.resolve({
+    items,
+    total: filtered.length,
+    data: { page, size, total: filtered.length },
+  });
+}
+
+/** 点评专家状态更改(当前为本地 mock,后期对接后端接口) */
+export function updateReviewExpertStatusMethod(
+  expertId: string,
+  { status }: { status: 0 | 1 },
+) {
+  const expert = MOCK_EXPERTS.find(
+    (item) => item.id === expertId || item.pid === expertId,
+  );
+  if (!expert) {
+    return Promise.reject(new Error('专家不存在'));
+  }
+  expert.status = status;
+  return Promise.resolve(true);
+}

+ 51 - 0
apps/smart-pharmacy/src/api/model/menu.ts

@@ -31,6 +31,57 @@ const accessMenuRouteMap = buildAccessMenuRouteMap(
   accessMenuRoutes as AccessMenuRoute[],
 );
 
+/**
+ * 本地写死的菜单树节点(后端 treeselect 未返回时合并进侧栏)。
+ * 后期后端配置相同 id 的菜单后,将优先使用接口返回的 label。
+ */
+export const HARDCODED_MENU_TREE_SELECT: TreeSelectMenuNode[] = [
+  {
+    id: '2500',
+    label: '处方点评',
+    children: [{ id: '2501', label: '点评专家' }],
+  },
+];
+
+/** 将本地写死菜单合并进后端 treeselect 结果 */
+export function mergeHardcodedMenuTree(
+  nodes?: TreeSelectMenuNode[],
+): TreeSelectMenuNode[] {
+  const backend = Array.isArray(nodes) ? [...nodes] : [];
+
+  for (const hard of HARDCODED_MENU_TREE_SELECT) {
+    const existing = backend.find((n) => String(n.id) === String(hard.id));
+    if (!existing) {
+      backend.push({ ...hard, children: hard.children ? [...hard.children] : undefined });
+      continue;
+    }
+    if (!hard.children?.length) continue;
+    existing.children ??= [];
+    for (const child of hard.children) {
+      if (
+        !existing.children.some((c) => String(c.id) === String(child.id))
+      ) {
+        existing.children.push(child);
+      }
+    }
+  }
+
+  return backend;
+}
+
+/** 角色权限未包含写死菜单 id 时,仍保留本地菜单(便于前期联调) */
+export function ensureHardcodedMenusVisible(
+  filtered: SystemModel.Menu[],
+  all: SystemModel.Menu[],
+): SystemModel.Menu[] {
+  const hardRoot = all.find((menu) => String(menu.id) === '2500');
+  if (!hardRoot) return filtered;
+  if (filtered.some((menu) => String(menu.id) === '2500')) return filtered;
+  return [...filtered, hardRoot].sort(
+    (a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999),
+  );
+}
+
 function buildAccessMenuRouteMap(
   menus: AccessMenuRoute[],
   map = new Map<string, AccessMenuRoute>(),

+ 100 - 0
apps/smart-pharmacy/src/views/prescription-review/expert/data.ts

@@ -0,0 +1,100 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { PrescriptionReviewModel } from '#/api/method/prescription-review';
+
+import { h } from 'vue';
+
+import { Button } from 'ant-design-vue';
+
+import { listUsersInstitutionMethodTree } from '#/api/method/system';
+import { maskPhone } from '#/utils/mask-phone';
+
+export function useReviewExpertSearchFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'ApiTreeSelect',
+      componentProps: {
+        allowClear: true,
+        api: listUsersInstitutionMethodTree,
+        childrenField: 'children',
+        class: 'w-full',
+        dropdownStyle: { maxHeight: 400, overflow: 'auto' },
+        labelField: 'name',
+        placeholder: '请搜索选择',
+        showSearch: true,
+        treeDefaultExpandAll: true,
+        treeNodeFilterProp: 'name',
+        valueField: 'pid',
+      },
+      fieldName: 'institutionId',
+      label: '所属机构',
+    },
+  ];
+}
+
+export function useReviewExpertTableColumns(
+  onNameClick?: (row: PrescriptionReviewModel.ReviewExpert) => void,
+  onStatusChange?: (
+    newStatus: 0 | 1,
+    row: PrescriptionReviewModel.ReviewExpert,
+  ) => PromiseLike<boolean | undefined>,
+): VxeTableGridOptions<PrescriptionReviewModel.ReviewExpert>['columns'] {
+  return [
+    {
+      field: 'access',
+      title: '系统账号',
+      minWidth: 120,
+    },
+    {
+      field: 'name',
+      minWidth: 100,
+      title: '姓名',
+      slots: {
+        default: ({ row }) =>
+          h(
+            Button,
+            {
+              size: 'small',
+              type: 'link',
+              onClick: () => onNameClick?.(row),
+            },
+            { default: () => row.name },
+          ),
+      },
+    },
+    {
+      field: 'hospitalName',
+      minWidth: 180,
+      title: '所属机构',
+    },
+    {
+      field: 'phone',
+      minWidth: 130,
+      title: '手机号码',
+      slots: {
+        default: ({ row }) => maskPhone(row.phone),
+      },
+    },
+    {
+      field: 'createUser',
+      minWidth: 100,
+      title: '创建人',
+    },
+    {
+      field: 'createTime',
+      minWidth: 160,
+      title: '创建时间',
+    },
+    {
+      cellRender: {
+        attrs: { beforeChange: onStatusChange },
+        props: { _props: { checkedValue: 0, unCheckedValue: 1 } },
+        name: onStatusChange ? 'CellSwitch' : 'CellTag',
+      },
+      field: 'status',
+      minWidth: 100,
+      title: '启用状态',
+    },
+  ];
+}

+ 121 - 0
apps/smart-pharmacy/src/views/prescription-review/expert/list.vue

@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+import type { Recordable } from '@vben/types';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { PrescriptionReviewModel } from '#/api';
+
+import { Page } from '@vben/common-ui';
+
+import { message, Modal, notification } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { listReviewExpertsMethod, updateReviewExpertStatusMethod } from '#/api';
+
+import {
+  useReviewExpertSearchFormSchema,
+  useReviewExpertTableColumns,
+} from './data';
+
+const [Grid] = useVbenVxeGrid({
+  formOptions: {
+    schema: useReviewExpertSearchFormSchema(),
+    submitOnChange: false,
+    wrapperClass: 'review-expert-search-form',
+  },
+  gridOptions: {
+    columns: useReviewExpertTableColumns(onNameClick, onStatusChange),
+    height: 'auto',
+    keepSource: true,
+    pagerConfig: {
+      pageSize: 2,
+      layouts: ['PrevPage', 'Jump', 'NextPage'],
+    },
+    proxyConfig: {
+      ajax: {
+        query({ page }, formValues) {
+          return listReviewExpertsMethod(
+            page.currentPage,
+            page.pageSize,
+            formValues,
+          );
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+    },
+    stripe: true,
+  } as VxeTableGridOptions<PrescriptionReviewModel.ReviewExpert>,
+});
+
+function onNameClick(row: PrescriptionReviewModel.ReviewExpert) {
+  message.info(`专家:${row.name}`);
+}
+
+function confirm(content: string, title: string) {
+  return new Promise((reslove, reject) => {
+    Modal.confirm({
+      content,
+      onCancel() {
+        reject(new Error('已取消'));
+      },
+      onOk() {
+        reslove(true);
+      },
+      title,
+    });
+  });
+}
+
+async function onStatusChange(
+  newStatus: 0 | 1,
+  row: PrescriptionReviewModel.ReviewExpert,
+) {
+  const status: Recordable<string> = {
+    0: '启用',
+    1: '禁用',
+  };
+  try {
+    await confirm(
+      `你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
+      `切换状态`,
+    );
+
+    try {
+      await updateReviewExpertStatusMethod(row.pid ?? row.id, {
+        status: newStatus,
+      });
+      notification.success({
+        message: '切换状态成功',
+      });
+      return true;
+    } catch (error: any) {
+      notification.error({
+        message: error.message || '切换状态失败',
+      });
+      return false;
+    }
+  } catch {
+    return false;
+  }
+}
+</script>
+<template>
+  <Page auto-content-height class="review-expert-page">
+    <Grid />
+  </Page>
+</template>
+<style scoped>
+.review-expert-page :deep(.vxe-grid--form-wrapper)::after {
+  display: block;
+  margin: -4px 0 12px;
+  color: rgb(0 0 0 / 45%);
+  content: '请至用户管理菜单添加';
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.review-expert-page :deep(.review-expert-search-form) {
+  margin-bottom: 0;
+}
+</style>