Bladeren bron

feat(@six/smart-pharmacy): 智慧药事系统第一版静态页面初始化

cmj 1 maand geleden
bovenliggende
commit
400b5354fa
87 gewijzigde bestanden met toevoegingen van 7004 en 0 verwijderingen
  1. 8 0
      apps/smart-pharmacy/.env
  2. 15 0
      apps/smart-pharmacy/.env.development
  3. 21 0
      apps/smart-pharmacy/.env.production
  4. 18 0
      apps/smart-pharmacy/index.html
  5. 46 0
      apps/smart-pharmacy/package.json
  6. 1 0
      apps/smart-pharmacy/postcss.config.mjs
  7. 88 0
      apps/smart-pharmacy/public/database/menu.json
  8. 0 0
      apps/smart-pharmacy/public/favicon.ico
  9. 107 0
      apps/smart-pharmacy/src/adapter/component/Avatar.vue
  10. 241 0
      apps/smart-pharmacy/src/adapter/component/AvatarTwo.vue
  11. 217 0
      apps/smart-pharmacy/src/adapter/component/index.ts
  12. 49 0
      apps/smart-pharmacy/src/adapter/form.ts
  13. 308 0
      apps/smart-pharmacy/src/adapter/vxe-table.ts
  14. 98 0
      apps/smart-pharmacy/src/api/index.ts
  15. 86 0
      apps/smart-pharmacy/src/api/method/access.ts
  16. 182 0
      apps/smart-pharmacy/src/api/method/business.ts
  17. 20 0
      apps/smart-pharmacy/src/api/method/common.ts
  18. 52 0
      apps/smart-pharmacy/src/api/method/operate.ts
  19. 68 0
      apps/smart-pharmacy/src/api/method/register.ts
  20. 284 0
      apps/smart-pharmacy/src/api/method/system.ts
  21. 159 0
      apps/smart-pharmacy/src/api/method/treatment.ts
  22. 43 0
      apps/smart-pharmacy/src/api/model/department.ts
  23. 46 0
      apps/smart-pharmacy/src/api/model/doctor.ts
  24. 23 0
      apps/smart-pharmacy/src/api/model/index.ts
  25. 49 0
      apps/smart-pharmacy/src/api/model/menu.ts
  26. 49 0
      apps/smart-pharmacy/src/api/model/operate.ts
  27. 40 0
      apps/smart-pharmacy/src/api/model/organization.ts
  28. 36 0
      apps/smart-pharmacy/src/api/model/patient.ts
  29. 32 0
      apps/smart-pharmacy/src/api/model/project.ts
  30. 59 0
      apps/smart-pharmacy/src/api/model/register.ts
  31. 25 0
      apps/smart-pharmacy/src/api/model/role.ts
  32. 23 0
      apps/smart-pharmacy/src/api/model/tab.ts
  33. 74 0
      apps/smart-pharmacy/src/api/model/treatmentDetail.ts
  34. 44 0
      apps/smart-pharmacy/src/api/model/user.ts
  35. 39 0
      apps/smart-pharmacy/src/app.vue
  36. 77 0
      apps/smart-pharmacy/src/bootstrap.ts
  37. 102 0
      apps/smart-pharmacy/src/components/import/database.modal.vue
  38. 84 0
      apps/smart-pharmacy/src/core/authentication/login.vue
  39. 7 0
      apps/smart-pharmacy/src/core/fallback/coming-soon.vue
  40. 9 0
      apps/smart-pharmacy/src/core/fallback/forbidden.vue
  41. 9 0
      apps/smart-pharmacy/src/core/fallback/internal-error.vue
  42. 11 0
      apps/smart-pharmacy/src/core/fallback/not-found.vue
  43. 9 0
      apps/smart-pharmacy/src/core/fallback/offline.vue
  44. 26 0
      apps/smart-pharmacy/src/layouts/auth.vue
  45. 105 0
      apps/smart-pharmacy/src/layouts/basic.vue
  46. 6 0
      apps/smart-pharmacy/src/layouts/index.ts
  47. 102 0
      apps/smart-pharmacy/src/locales/index.ts
  48. 5 0
      apps/smart-pharmacy/src/locales/langs/zh-CN/authentication.json
  49. 5 0
      apps/smart-pharmacy/src/locales/langs/zh-CN/common.json
  50. 9 0
      apps/smart-pharmacy/src/locales/langs/zh-CN/page.json
  51. 25 0
      apps/smart-pharmacy/src/locales/langs/zh-CN/prescription.json
  52. 68 0
      apps/smart-pharmacy/src/locales/langs/zh-CN/system.json
  53. 14 0
      apps/smart-pharmacy/src/locales/langs/zh-CN/table.json
  54. 31 0
      apps/smart-pharmacy/src/main.ts
  55. 42 0
      apps/smart-pharmacy/src/preferences.ts
  56. 50 0
      apps/smart-pharmacy/src/router/access.ts
  57. 147 0
      apps/smart-pharmacy/src/router/guard.ts
  58. 37 0
      apps/smart-pharmacy/src/router/index.ts
  59. 63 0
      apps/smart-pharmacy/src/router/routes/core.ts
  60. 40 0
      apps/smart-pharmacy/src/router/routes/index.ts
  61. 77 0
      apps/smart-pharmacy/src/store/auth.ts
  62. 1 0
      apps/smart-pharmacy/src/store/index.ts
  63. 187 0
      apps/smart-pharmacy/src/views/prescription/management/data.ts
  64. 421 0
      apps/smart-pharmacy/src/views/prescription/management/detail.vue
  65. 173 0
      apps/smart-pharmacy/src/views/prescription/management/list.vue
  66. 94 0
      apps/smart-pharmacy/src/views/prescription/management/modules/form.vue
  67. 98 0
      apps/smart-pharmacy/src/views/system/enterprise/data.ts
  68. 108 0
      apps/smart-pharmacy/src/views/system/enterprise/list.vue
  69. 49 0
      apps/smart-pharmacy/src/views/system/enterprise/modules/form.vue
  70. 213 0
      apps/smart-pharmacy/src/views/system/organization/data.ts
  71. 164 0
      apps/smart-pharmacy/src/views/system/organization/list.vue
  72. 90 0
      apps/smart-pharmacy/src/views/system/organization/modules/form.vue
  73. 128 0
      apps/smart-pharmacy/src/views/system/role/data.ts
  74. 203 0
      apps/smart-pharmacy/src/views/system/role/list.vue
  75. 35 0
      apps/smart-pharmacy/src/views/system/role/mock.ts
  76. 251 0
      apps/smart-pharmacy/src/views/system/role/modules/form.vue
  77. 108 0
      apps/smart-pharmacy/src/views/system/tisane/data.ts
  78. 114 0
      apps/smart-pharmacy/src/views/system/tisane/list.vue
  79. 49 0
      apps/smart-pharmacy/src/views/system/tisane/modules/form.vue
  80. 229 0
      apps/smart-pharmacy/src/views/system/user/data.ts
  81. 186 0
      apps/smart-pharmacy/src/views/system/user/list.vue
  82. 154 0
      apps/smart-pharmacy/src/views/system/user/mock.ts
  83. 91 0
      apps/smart-pharmacy/src/views/system/user/modules/form.vue
  84. 1 0
      apps/smart-pharmacy/tailwind.config.mjs
  85. 12 0
      apps/smart-pharmacy/tsconfig.json
  86. 10 0
      apps/smart-pharmacy/tsconfig.node.json
  87. 25 0
      apps/smart-pharmacy/vite.config.mts

+ 8 - 0
apps/smart-pharmacy/.env

@@ -0,0 +1,8 @@
+# 应用标题
+VITE_APP_TITLE=智慧药事系统
+
+# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
+VITE_APP_NAMESPACE=@six.admin/smart-pharmacy
+
+# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
+VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key

+ 15 - 0
apps/smart-pharmacy/.env.development

@@ -0,0 +1,15 @@
+# 端口号
+VITE_PORT=5666
+
+VITE_BASE=/
+
+# 接口地址
+VITE_GLOB_API_URL=
+# 智能辅诊接口地址
+VITE_GLOB_API_SMART-PHARMACY=/wf
+
+# 是否打开 devtools,true 为打开,false 为关闭
+VITE_DEVTOOLS=true
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true

+ 21 - 0
apps/smart-pharmacy/.env.production

@@ -0,0 +1,21 @@
+VITE_BASE=/wf/p/
+
+# 接口地址
+VITE_GLOB_API_URL=
+# 智能辅诊接口地址
+VITE_GLOB_API_SMART-PHARMACY=/wf
+
+# 是否开启压缩,可以设置为 none, brotli, gzip
+VITE_COMPRESS=brotli,gzip
+
+# 是否开启 PWA
+VITE_PWA=false
+
+# vue-router 的模式
+VITE_ROUTER_HISTORY=hash
+
+# 是否注入全局loading
+VITE_INJECT_APP_LOADING=true
+
+# 打包后是否生成dist.zip
+VITE_ARCHIVER=true

+ 18 - 0
apps/smart-pharmacy/index.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta
+      name="viewport"
+      content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
+    />
+    <title><%= VITE_APP_TITLE %></title>
+    <link rel="icon" href="/favicon.ico" />
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 46 - 0
apps/smart-pharmacy/package.json

@@ -0,0 +1,46 @@
+{
+  "name": "@six/smart-pharmacy",
+  "version": "0.0.0",
+  "homepage": "",
+  "bugs": "",
+  "license": "MIT",
+  "type": "module",
+  "scripts": {
+    "build": "pnpm vite build --mode production",
+    "build:analyze": "pnpm vite build --mode analyze",
+    "dev": "pnpm vite --mode development",
+    "preview": "vite preview",
+    "typecheck": "vue-tsc --noEmit --skipLibCheck"
+  },
+  "imports": {
+    "#/*": "./src/*"
+  },
+  "dependencies": {
+    "@six/request": "workspace:*",
+    "@vben-core/menu-ui": "workspace:*",
+    "@vben/access": "workspace:*",
+    "@vben/common-ui": "workspace:*",
+    "@vben/constants": "workspace:*",
+    "@vben/hooks": "workspace:*",
+    "@vben/icons": "workspace:*",
+    "@vben/layouts": "workspace:*",
+    "@vben/locales": "workspace:*",
+    "@vben/plugins": "workspace:*",
+    "@vben/preferences": "workspace:*",
+    "@vben/stores": "workspace:*",
+    "@vben/styles": "workspace:*",
+    "@vben/types": "workspace:*",
+    "@vben/utils": "workspace:*",
+    "@vueuse/core": "catalog:",
+    "@vueuse/router": "catalog:",
+    "alova": "catalog:",
+    "ant-design-vue": "catalog:",
+    "dayjs": "catalog:",
+    "pinia": "catalog:",
+    "vue": "catalog:",
+    "vue-router": "catalog:"
+  },
+  "devDependencies": {
+    "@vben-core/typings": "workspace:*"
+  }
+}

+ 1 - 0
apps/smart-pharmacy/postcss.config.mjs

@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config/postcss';

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

@@ -0,0 +1,88 @@
+[
+  {
+    "meta": {
+      "icon": "ion:settings-outline",
+      "order": 9997,
+      "title": "system.title"
+    },
+    "name": "System",
+    "path": "/system",
+    "children": [
+      {
+        "path": "/system/organization",
+        "name": "MedicalOrganization",
+        "meta": {
+          "icon": "mdi:hospital-building",
+          "title": "医疗机构管理"
+        },
+        "component": "/system/organization/list"
+      },
+      {
+        "path": "/system/enterprise",
+        "name": "SystemEnterprise",
+        "meta": {
+          "icon": "mdi:account-group",
+          "title": "企业管理"
+        },
+        "component": "/system/enterprise/list"
+      },
+      {
+        "path": "/system/tisane",
+        "name": "SystemTisane",
+        "meta": {
+          "icon": "mdi:pill-multiple",
+          "title": "煎药中心管理"
+        },
+        "component": "/system/tisane/list"
+      },
+      {
+        "path": "/system/role",
+        "name": "SystemRole",
+        "meta": {
+          "icon": "charm:organisation",
+          "title": "角色管理"
+        },
+        "component": "/system/role/list"
+      },
+      {
+        "path": "/system/user",
+        "name": "SystemUser",
+        "meta": {
+          "icon": "mdi:account-group",
+          "title": "用户管理"
+        },
+        "component": "/system/user/list"
+      }
+    ]
+  },
+  {
+    "meta": {
+      "icon": "ion:settings-outline",
+      "order": 9998,
+      "title": "处方管理"
+    },
+    "name": "Prescription",
+    "path": "/prescription",
+    "children": [
+      {
+        "path": "/prescription/management",
+        "name": "PrescriptionManagement",
+        "meta": {
+          "icon": "mdi:prescription",
+          "title": "处方列表"
+        },
+        "component": "/prescription/management/list"
+      },
+      {
+        "path": "/prescription/detail/:id",
+        "name": "PrescriptionPreview",
+        "meta": {
+          "title": "处方预览",
+          "hideInTab": true,
+          "hideInMenu": true
+        },
+        "component": "/prescription/management/detail"
+      }
+    ]
+  }
+]

+ 0 - 0
apps/smart-pharmacy/public/favicon.ico


+ 107 - 0
apps/smart-pharmacy/src/adapter/component/Avatar.vue

@@ -0,0 +1,107 @@
+<script setup lang="ts">
+import type { UploadChangeParam } from 'ant-design-vue';
+
+import { ref, watchEffect } from 'vue';
+
+import { useRequest } from 'alova/client';
+import { Modal, Upload } from 'ant-design-vue';
+
+import { uploadFileMethod } from '#/api/method/common';
+
+interface UploadFile extends File {
+  uid?: string;
+  status: 'done' | 'error' | 'removed' | 'success' | 'uploading';
+  thumbUrl: string;
+  url: string;
+}
+
+defineOptions({
+  inheritAttrs: false,
+});
+
+function getBase64(img: Blob, callback: (base64Url: string) => void) {
+  const reader = new FileReader();
+  reader.addEventListener('load', () => callback(reader.result as string), {
+    once: true,
+  });
+  reader.readAsDataURL(img);
+}
+
+const { loading, send: upload } = useRequest(uploadFileMethod, {
+  immediate: false,
+});
+
+const avatar = defineModel<string | void>('value', { default: void 0 });
+const fileList = ref<UploadFile[]>([]);
+watchEffect(() => {
+  const url = avatar.value ?? '';
+  if (url) {
+    const name = url.split('/').pop() ?? 'avatar.png';
+    fileList.value = [{ uid: '-1', status: 'done', name, url } as UploadFile];
+  } else {
+    fileList.value = [];
+  }
+});
+
+const previewVisible = ref(false);
+const previewImage = ref('');
+const handleCancel = () => {
+  previewVisible.value = false;
+};
+const onPreviewHandle = async (file: UploadFile) => {
+  previewImage.value = file.url || file.thumbUrl;
+  previewVisible.value = true;
+};
+
+const onChangeHandle = async (info: UploadChangeParam<UploadFile>) => {
+  info.file.status ??= 'uploading';
+  if (info.file.status === 'uploading') {
+    getBase64(info.file, (thumbUrl) => {
+      info.file.thumbUrl = thumbUrl;
+    });
+    try {
+      const url = await upload(info.file);
+      info.file.status = 'done';
+      avatar.value = url;
+    } catch {
+      info.file.status = 'error';
+      avatar.value = void 0;
+    }
+  }
+};
+
+const onRemoveHandle = () => {
+  avatar.value = void 0;
+};
+</script>
+
+<template>
+  <div>
+    <Upload
+      v-bind="$attrs"
+      v-model:file-list="fileList"
+      accept="image/*"
+      :max-count="1"
+      name="avatar"
+      list-type="picture-card"
+      class="avatar-uploader"
+      :disabled="loading"
+      :before-upload="() => false"
+      @change="onChangeHandle"
+      @remove="onRemoveHandle"
+      @preview="onPreviewHandle"
+    >
+      <div v-if="!fileList?.length" class="ant-upload-text">上传</div>
+    </Upload>
+    <Modal
+      :open="previewVisible"
+      title="图片预览"
+      :footer="null"
+      @cancel="handleCancel"
+    >
+      <img alt="example" style="width: 100%" :src="previewImage" />
+    </Modal>
+  </div>
+</template>
+
+<style scoped></style>

+ 241 - 0
apps/smart-pharmacy/src/adapter/component/AvatarTwo.vue

@@ -0,0 +1,241 @@
+<script setup lang="ts">
+import { nextTick, ref, watchEffect } from 'vue';
+
+import { useRequest } from 'alova/client';
+import { Modal, Upload } from 'ant-design-vue';
+
+import { uploadFileMethod } from '#/api/method/common';
+
+defineOptions({
+  inheritAttrs: false,
+});
+
+function getBase64(img: Blob, callback: (base64Url: string) => void) {
+  const reader = new FileReader();
+  reader.addEventListener('load', () => callback(reader.result as string), {
+    once: true,
+  });
+  reader.readAsDataURL(img);
+}
+
+const { loading, send: upload } = useRequest(uploadFileMethod, {
+  immediate: false,
+});
+
+const avatar = defineModel<string | void>('value', { default: void 0 });
+const fileList = ref<any[]>([]);
+watchEffect(() => {
+  const url = avatar.value ?? '';
+  if (url) {
+    const name = url.split('/').pop() ?? 'avatar.png';
+    fileList.value = [
+      {
+        uid: '-1',
+        status: 'done',
+        name,
+        url,
+        thumbUrl: url,
+      } as any,
+    ];
+  } else {
+    fileList.value = [];
+  }
+});
+
+const previewVisible = ref(false);
+const previewImage = ref('');
+const handleCancel = () => {
+  previewVisible.value = false;
+};
+const onPreviewHandle = async (file: any) => {
+  previewImage.value = file.url || file.thumbUrl;
+  previewVisible.value = true;
+};
+
+const onRemoveHandle = () => {
+  fileList.value = [];
+  avatar.value = void 0;
+};
+
+const sourceModalVisible = ref(false);
+const cameraInputRef = ref<HTMLInputElement | null>(null);
+const albumInputRef = ref<HTMLInputElement | null>(null);
+
+// 打开选择来源弹窗(只在没有图片时触发)
+const handleUploadClick = (e: Event) => {
+  // 如果已经有图片,不处理点击
+  if (fileList.value.length > 0) return;
+
+  e.preventDefault();
+  e.stopPropagation();
+  e.stopImmediatePropagation();
+
+  // 立即显示弹窗,不使用 nextTick
+  setTimeout(() => {
+    sourceModalVisible.value = true;
+  }, 10);
+};
+
+// 选择拍照
+const handleCameraClick = (e: Event) => {
+  e.preventDefault();
+  e.stopPropagation();
+  sourceModalVisible.value = false;
+  nextTick(() => {
+    cameraInputRef.value?.click();
+  });
+};
+
+// 选择相册
+const handleAlbumClick = (e: Event) => {
+  e.preventDefault();
+  e.stopPropagation();
+  sourceModalVisible.value = false;
+  nextTick(() => {
+    albumInputRef.value?.click();
+  });
+};
+
+// 处理文件选择(两个input共用)
+const handleFileChange = async (e: Event) => {
+  e.preventDefault();
+  e.stopPropagation();
+
+  const target = e.target as HTMLInputElement;
+  const file = target.files?.[0];
+  if (!file) return;
+
+  const fileId = Date.now().toString();
+
+  // 第一步:立即生成 base64 预览(瞬间显示)
+  getBase64(file as Blob, (base64Url) => {
+    // 立即显示预览(瞬间显示)
+    fileList.value = [
+      {
+        uid: fileId,
+        name: file.name,
+        status: 'uploading', // 显示上传中状态
+        url: base64Url, // 立即显示base64预览
+        thumbUrl: base64Url,
+      } as any,
+    ];
+  });
+
+  // 第二步:同时开始上传文件
+  try {
+    const url = await upload(file);
+    // 上传成功,更新URL(使用服务器返回的URL)
+    fileList.value = [
+      {
+        uid: fileId,
+        name: file.name,
+        status: 'done',
+        url,
+        thumbUrl: url,
+      } as any,
+    ];
+
+    avatar.value = url;
+  } catch (error) {
+    // 上传失败,移除文件
+    fileList.value = fileList.value.filter((item) => item.uid !== fileId);
+    console.error('上传失败:', error);
+  }
+
+  // 清空当前input,以便可以重复选择同一文件
+  if (target === cameraInputRef.value) {
+    cameraInputRef.value.value = '';
+  } else if (target === albumInputRef.value) {
+    albumInputRef.value.value = '';
+  }
+};
+</script>
+
+<template>
+  <div>
+    <!-- 隐藏的原生input:拍照 -->
+    <input
+      ref="cameraInputRef"
+      type="file"
+      accept="image/*"
+      capture="environment"
+      style="display: none"
+      @change="handleFileChange"
+    />
+
+    <!-- 隐藏的原生input:相册 -->
+    <input
+      ref="albumInputRef"
+      type="file"
+      accept="image/*"
+      style="display: none"
+      @change="handleFileChange"
+    />
+
+    <!-- Upload 组件用于图片展示和交互 -->
+    <Upload
+      v-bind="$attrs"
+      v-model:file-list="fileList"
+      accept="image/*"
+      :max-count="1"
+      list-type="picture-card"
+      :disabled="loading"
+      :before-upload="() => false"
+      :custom-request="() => {}"
+      @remove="onRemoveHandle"
+      @preview="onPreviewHandle"
+    >
+      <template v-if="!fileList?.length">
+        <button
+          type="button"
+          class="ant-upload-text"
+          @mousedown.capture.stop="handleUploadClick"
+          @touchstart.capture.stop="handleUploadClick"
+          @pointerdown.capture.stop="handleUploadClick"
+        >
+          上传
+        </button>
+      </template>
+    </Upload>
+
+    <!-- 选择来源弹窗 -->
+    <Modal
+      v-model:open="sourceModalVisible"
+      title="选择图片来源"
+      :footer="null"
+      :width="300"
+      :mask-closable="false"
+      :closable="true"
+      :keyboard="false"
+    >
+      <div class="flex flex-col gap-3 p-4">
+        <button
+          type="button"
+          class="flex h-12 items-center justify-center rounded-lg bg-blue-500 text-white transition-colors hover:bg-blue-600"
+          @click.stop.prevent="handleCameraClick"
+        >
+          拍照
+        </button>
+        <button
+          type="button"
+          class="flex h-12 items-center justify-center rounded-lg bg-gray-500 text-white transition-colors hover:bg-gray-600"
+          @click.stop.prevent="handleAlbumClick"
+        >
+          从相册选择
+        </button>
+      </div>
+    </Modal>
+
+    <!-- 图片预览弹窗 -->
+    <Modal
+      :open="previewVisible"
+      title="图片预览"
+      :footer="null"
+      @cancel="handleCancel"
+    >
+      <img alt="example" style="width: 100%" :src="previewImage" />
+    </Modal>
+  </div>
+</template>
+
+<style scoped></style>

+ 217 - 0
apps/smart-pharmacy/src/adapter/component/index.ts

@@ -0,0 +1,217 @@
+/**
+ * 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
+ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
+ */
+
+import type { Component } from 'vue';
+
+import type { BaseFormComponentType } from '@vben/common-ui';
+import type { Recordable } from '@vben/types';
+
+import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
+
+import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import { notification } from 'ant-design-vue';
+
+const AutoComplete = defineAsyncComponent(
+  () => import('ant-design-vue/es/auto-complete'),
+);
+const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
+const Checkbox = defineAsyncComponent(
+  () => import('ant-design-vue/es/checkbox'),
+);
+const CheckboxGroup = defineAsyncComponent(() =>
+  import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
+);
+const DatePicker = defineAsyncComponent(
+  () => import('ant-design-vue/es/date-picker'),
+);
+const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
+const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
+const InputNumber = defineAsyncComponent(
+  () => import('ant-design-vue/es/input-number'),
+);
+const InputPassword = defineAsyncComponent(() =>
+  import('ant-design-vue/es/input').then((res) => res.InputPassword),
+);
+const Mentions = defineAsyncComponent(
+  () => import('ant-design-vue/es/mentions'),
+);
+const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
+const RadioGroup = defineAsyncComponent(() =>
+  import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
+);
+const RangePicker = defineAsyncComponent(() =>
+  import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
+);
+const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
+const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
+const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
+const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
+const Textarea = defineAsyncComponent(() =>
+  import('ant-design-vue/es/input').then((res) => res.Textarea),
+);
+const TimePicker = defineAsyncComponent(
+  () => import('ant-design-vue/es/time-picker'),
+);
+const TreeSelect = defineAsyncComponent(
+  () => import('ant-design-vue/es/tree-select'),
+);
+const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
+const Avatar = defineAsyncComponent(() => import('./Avatar.vue'));
+const AvatarTwo = defineAsyncComponent(() => import('./AvatarTwo.vue'));
+
+const withDefaultPlaceholder = <T extends Component>(
+  component: T,
+  type: 'input' | 'select',
+  componentProps: Recordable<any> = {},
+) => {
+  return defineComponent({
+    name: component.name,
+    inheritAttrs: false,
+    setup: (props: any, { attrs, expose, slots }) => {
+      const placeholder =
+        props?.placeholder ||
+        attrs?.placeholder ||
+        $t(`ui.placeholder.${type}`);
+      // 透传组件暴露的方法
+      const innerRef = ref();
+      expose(
+        new Proxy(
+          {},
+          {
+            get: (_target, key) => innerRef.value?.[key],
+            has: (_target, key) => key in (innerRef.value || {}),
+          },
+        ),
+      );
+      return () =>
+        h(
+          component,
+          { ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
+          slots,
+        );
+    },
+  });
+};
+
+// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
+export type ComponentType =
+  | 'ApiSelect'
+  | 'ApiTreeSelect'
+  | 'AutoComplete'
+  | 'Avatar'
+  | 'AvatarTwo'
+  | 'Checkbox'
+  | 'CheckboxGroup'
+  | 'DatePicker'
+  | 'DefaultButton'
+  | 'Divider'
+  | 'IconPicker'
+  | 'Input'
+  | 'InputNumber'
+  | 'InputPassword'
+  | 'Mentions'
+  | 'PrimaryButton'
+  | 'Radio'
+  | 'RadioGroup'
+  | 'RangePicker'
+  | 'Rate'
+  | 'Select'
+  | 'Space'
+  | 'Switch'
+  | 'Textarea'
+  | 'TimePicker'
+  | 'TreeSelect'
+  | 'Upload'
+  | BaseFormComponentType;
+
+async function initComponentAdapter() {
+  const components: Partial<Record<ComponentType, Component>> = {
+    // 如果你的组件体积比较大,可以使用异步加载
+    // Button: () =>
+    // import('xxx').then((res) => res.Button),
+    ApiSelect: withDefaultPlaceholder(
+      {
+        ...ApiComponent,
+        name: 'ApiSelect',
+      },
+      'select',
+      {
+        component: Select,
+        loadingSlot: 'suffixIcon',
+        visibleEvent: 'onDropdownVisibleChange',
+        modelPropName: 'value',
+      },
+    ),
+    ApiTreeSelect: withDefaultPlaceholder(
+      {
+        ...ApiComponent,
+        name: 'ApiTreeSelect',
+      },
+      'select',
+      {
+        component: TreeSelect,
+        fieldNames: { label: 'label', value: 'value', children: 'children' },
+        loadingSlot: 'suffixIcon',
+        modelPropName: 'value',
+        optionsPropName: 'treeData',
+        visibleEvent: 'onVisibleChange',
+      },
+    ),
+    AutoComplete,
+    Checkbox,
+    CheckboxGroup,
+    DatePicker,
+    // 自定义默认按钮
+    DefaultButton: (props, { attrs, slots }) => {
+      return h(Button, { ...props, attrs, type: 'default' }, slots);
+    },
+    Divider,
+    IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
+      iconSlot: 'addonAfter',
+      inputComponent: Input,
+      modelValueProp: 'value',
+    }),
+    Input: withDefaultPlaceholder(Input, 'input'),
+    InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
+    InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
+    Mentions: withDefaultPlaceholder(Mentions, 'input'),
+    // 自定义主要按钮
+    PrimaryButton: (props, { attrs, slots }) => {
+      return h(Button, { ...props, attrs, type: 'primary' }, slots);
+    },
+    Radio,
+    RadioGroup,
+    RangePicker,
+    Rate,
+    Select: withDefaultPlaceholder(Select, 'select'),
+    Space,
+    Switch,
+    Textarea: withDefaultPlaceholder(Textarea, 'input'),
+    TimePicker,
+    TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
+    Upload,
+    Avatar,
+    AvatarTwo,
+  };
+
+  // 将组件注册到全局共享状态中
+  globalShareState.setComponents(components);
+
+  // 定义全局共享状态中的消息提示
+  globalShareState.defineMessage({
+    // 复制成功消息提示
+    copyPreferencesSuccess: (title, content) => {
+      notification.success({
+        description: content,
+        message: title,
+        placement: 'bottomRight',
+      });
+    },
+  });
+}
+
+export { initComponentAdapter };

+ 49 - 0
apps/smart-pharmacy/src/adapter/form.ts

@@ -0,0 +1,49 @@
+import type {
+  VbenFormSchema as FormSchema,
+  VbenFormProps,
+} from '@vben/common-ui';
+
+import type { ComponentType } from './component';
+
+import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+async function initSetupVbenForm() {
+  setupVbenForm<ComponentType>({
+    config: {
+      // ant design vue组件库默认都是 v-model:value
+      baseModelPropName: 'value',
+
+      // 一些组件是 v-model:checked 或者 v-model:fileList
+      modelPropNameMap: {
+        Checkbox: 'checked',
+        Radio: 'checked',
+        Switch: 'checked',
+        Upload: 'fileList',
+      },
+    },
+    defineRules: {
+      // 输入项目必填国际化适配
+      required: (value, _params, ctx) => {
+        if (value === undefined || value === null || value.length === 0) {
+          return $t('ui.formRules.required', [ctx.label]);
+        }
+        return true;
+      },
+      // 选择项目必填国际化适配
+      selectRequired: (value, _params, ctx) => {
+        if (value === undefined || value === null) {
+          return $t('ui.formRules.selectRequired', [ctx.label]);
+        }
+        return true;
+      },
+    },
+  });
+}
+
+const useVbenForm = useForm<ComponentType>;
+
+export { initSetupVbenForm, useVbenForm, z };
+
+export type VbenFormSchema = FormSchema<ComponentType>;
+export type { VbenFormProps };

+ 308 - 0
apps/smart-pharmacy/src/adapter/vxe-table.ts

@@ -0,0 +1,308 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+import type { Recordable } from '@vben/types';
+
+import type { ComponentType } from './component';
+
+import { h, resolveDirective, withDirectives } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+import { $te } from '@vben/locales';
+import {
+  setupVbenVxeTable,
+  useVbenVxeGrid as useGrid,
+} from '@vben/plugins/vxe-table';
+import { get, isFunction, isString } from '@vben/utils';
+
+import { objectOmit } from '@vueuse/core';
+import { Button, Image, Popconfirm, Switch, Tag } from 'ant-design-vue';
+
+import { $t } from '#/locales';
+
+import { useVbenForm } from './form';
+
+setupVbenVxeTable({
+  configVxeTable: (vxeUI) => {
+    vxeUI.setConfig({
+      grid: {
+        align: 'center',
+        border: false,
+        columnConfig: {
+          resizable: true,
+        },
+        minHeight: 180,
+        formConfig: {
+          // 全局禁用vxe-table的表单配置,使用formOptions
+          enabled: false,
+        },
+        proxyConfig: {
+          autoLoad: true,
+          response: {
+            result: 'items',
+            total: 'total',
+            list: 'items',
+          },
+          showActiveMsg: true,
+          showResponseMsg: false,
+        },
+        round: true,
+        showOverflow: true,
+        size: 'small',
+      } as VxeTableGridOptions,
+    });
+
+    if (import.meta.env.DEV) {
+      // 解决vxeTable在热更新时可能会出错的问题
+      vxeUI.renderer.forEach((_item, key) => {
+        if (key.startsWith('Cell')) {
+          vxeUI.renderer.delete(key);
+        }
+      });
+    }
+
+    // 表格配置项可以用 cellRender: { name: 'CellImage' },
+    vxeUI.renderer.add('CellImage', {
+      renderTableDefault(_renderOpts, params) {
+        const { column, row } = params;
+        return h(Image, { src: row[column.field] });
+      },
+    });
+
+    // 表格配置项可以用 cellRender: { name: 'CellLink' },
+    vxeUI.renderer.add('CellLink', {
+      renderTableDefault(renderOpts) {
+        const { props } = renderOpts;
+        return h(
+          Button,
+          { size: 'small', type: 'link' },
+          { default: () => props?.text },
+        );
+      },
+    });
+
+    // 单元格渲染: Tag
+    vxeUI.renderer.add('CellTag', {
+      renderTableDefault({ options, props }, { column, row }) {
+        const value = get(row, column.field);
+        const tagOptions = options ?? [
+          { color: 'success', label: $t('common.enabled'), value: 1 },
+          { color: 'error', label: $t('common.disabled'), value: 0 },
+        ];
+        const tagItem = tagOptions.find((item) => item.value === value);
+        return h(
+          Tag,
+          {
+            ...props,
+            ...objectOmit(tagItem ?? {}, ['label']),
+          },
+          { default: () => tagItem?.label ?? value },
+        );
+      },
+    });
+
+    vxeUI.renderer.add('CellSwitch', {
+      renderTableDefault({ attrs, props }, { column, row }) {
+        const loadingKey = `__loading_${column.field}`;
+        const { accessRole, accessCode, _props } = props ?? {};
+        const finallyProps = {
+          checkedChildren: $t('common.enabled'),
+          checkedValue: 1,
+          unCheckedChildren: $t('common.disabled'),
+          unCheckedValue: 0,
+          ..._props,
+          checked: row[column.field],
+          loading: row[loadingKey] ?? false,
+          'onUpdate:checked': onChange,
+        };
+        async function onChange(newVal: any) {
+          row[loadingKey] = true;
+          try {
+            const result = await attrs?.beforeChange?.(newVal, row);
+            if (result !== false) {
+              row[column.field] = newVal;
+            }
+          } finally {
+            row[loadingKey] = false;
+          }
+        }
+        const access = resolveDirective('access');
+        const modifiers = { disabled: true };
+        return withDirectives(
+          h(Switch, finallyProps),
+          [
+            accessCode && [access, accessCode, 'code', modifiers],
+            accessRole && [access, accessRole, 'role', modifiers],
+          ].filter(Boolean),
+        );
+      },
+    });
+
+    /**
+     * 注册表格的操作按钮渲染器
+     */
+    vxeUI.renderer.add('CellOperation', {
+      renderTableDefault({ attrs, options, props }, { column, row }) {
+        const defaultProps = { size: 'small', type: 'link', ...props };
+        let align = 'end';
+        switch (column.align) {
+          case 'center': {
+            align = 'center';
+            break;
+          }
+          case 'left': {
+            align = 'start';
+            break;
+          }
+          default: {
+            align = 'end';
+            break;
+          }
+        }
+        const presets: Recordable<Recordable<any>> = {
+          delete: {
+            danger: true,
+            text: $t('common.delete'),
+          },
+          edit: {
+            text: $t('common.edit'),
+          },
+          set: {
+            text: $t('common.set'),
+          }
+        };
+        const operations: Array<Recordable<any>> = (
+          options || ['edit', 'delete']
+        )
+          .map((opt) => {
+            if (isString(opt)) {
+              return presets[opt]
+                ? { code: opt, ...presets[opt], ...defaultProps }
+                : {
+                    code: opt,
+                    text: $te(`common.${opt}`) ? $t(`common.${opt}`) : opt,
+                    ...defaultProps,
+                  };
+            } else {
+              return { ...defaultProps, ...presets[opt.code], ...opt };
+            }
+          })
+          .map((opt) => {
+            const optBtn: Recordable<any> = {};
+            Object.keys(opt).forEach((key) => {
+              optBtn[key] = isFunction(opt[key]) ? opt[key](row) : opt[key];
+            });
+            return optBtn;
+          })
+          .filter((opt) => opt.show !== false);
+
+        function renderBtn(opt: Recordable<any>, listen = true) {
+          return h(
+            Button,
+            {
+              ...props,
+              ...opt,
+              icon: undefined,
+              onClick: listen
+                ? () =>
+                    attrs?.onClick?.({
+                      code: opt.code,
+                      row,
+                    })
+                : undefined,
+            },
+            {
+              default: () => {
+                const content = [];
+                if (opt.icon) {
+                  content.push(
+                    h(IconifyIcon, { class: 'size-5', icon: opt.icon }),
+                  );
+                }
+                content.push(opt.text);
+                return content;
+              },
+            },
+          );
+        }
+
+        function renderConfirm(opt: Recordable<any>) {
+          let viewportWrapper: HTMLElement | null = null;
+          return h(
+            Popconfirm,
+            {
+              /**
+               * 当popconfirm用在固定列中时,将固定列作为弹窗的容器时可能会因为固定列较窄而无法容纳弹窗
+               * 将表格主体区域作为弹窗容器时又会因为固定列的层级较高而遮挡弹窗
+               * 将body或者表格视口区域作为弹窗容器时又会导致弹窗无法跟随表格滚动。
+               * 鉴于以上各种情况,一种折中的解决方案是弹出层展示时,禁止操作表格的滚动条。
+               * 这样既解决了弹窗的遮挡问题,又不至于让弹窗随着表格的滚动而跑出视口区域。
+               */
+              getPopupContainer(el) {
+                viewportWrapper = el.closest('.vxe-table--viewport-wrapper');
+                return document.body;
+              },
+              placement: 'topLeft',
+              title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']),
+              ...props,
+              ...opt,
+              icon: undefined,
+              onOpenChange: (open: boolean) => {
+                // 当弹窗打开时,禁止表格的滚动
+                if (open) {
+                  viewportWrapper?.style.setProperty('pointer-events', 'none');
+                } else {
+                  viewportWrapper?.style.removeProperty('pointer-events');
+                }
+              },
+              onConfirm: () => {
+                attrs?.onClick?.({
+                  code: opt.code,
+                  row,
+                });
+              },
+            },
+            {
+              default: () => renderBtn({ ...opt }, false),
+              description: () =>
+                h(
+                  'div',
+                  { class: 'truncate' },
+                  $t('ui.actionMessage.deleteConfirm', [
+                    row[attrs?.nameField || 'name'],
+                  ]),
+                ),
+            },
+          );
+        }
+
+        const btns = operations.map((opt) =>
+          opt.code === 'delete' ? renderConfirm(opt) : renderBtn(opt),
+        );
+        return h(
+          'div',
+          {
+            class: 'flex table-operations',
+            style: { justifyContent: align },
+          },
+          btns,
+        );
+      },
+    });
+
+    // 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
+    // vxeUI.formats.add
+  },
+  useVbenForm,
+});
+
+export const useVbenVxeGrid = <T extends Record<string, any>>(
+  ...rest: Parameters<typeof useGrid<T, ComponentType>>
+) => useGrid<T, ComponentType>(...rest);
+
+export type OnActionClickParams<T = Recordable<any>> = {
+  code: string;
+  row: T;
+};
+export type OnActionClickFn<T = Recordable<any>> = (
+  params: OnActionClickParams<T>,
+) => void;
+export type * from '@vben/plugins/vxe-table';

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

@@ -0,0 +1,98 @@
+import type { Recordable } from '@vben-core/typings';
+
+import createRequestClient from '@six/request';
+import { message } from 'ant-design-vue';
+
+import { useAuthStore } from '#/store';
+
+import '@six/request/alova';
+
+export * from './method/access';
+export * from './method/business';
+export * from './method/common';
+export * from './method/system';
+
+export const http = createRequestClient({
+  id: import.meta.env.VITE_APP_NAMESPACE?.split('/').pop() ?? 'health-remedy',
+  // 本地的开发地址
+  // baseURL: 'http://192.168.1.16:8039',
+  transform(body, method) {
+    /* prettier-ignore */
+    if (body === null || typeof body !== 'object') return { code: 0, data: body, message: 'ok' };
+    const { ResultCode: code, ResultInfo: message, Data: data, ...r } = body;
+    const result = { code, message, data, ...r };
+
+    if (
+      result.data?.TotalPageCount !== void 0 &&
+      Array.isArray(result.data?.Items)
+    ) {
+      const {
+        // TotalPageCount: total,
+        TotalRecordCount: total,
+        PageIndex: page,
+        PageSize: size,
+        Items: items,
+      } = result.data;
+      result.data = {
+        total,
+        items,
+        data: { page, size, total },
+      };
+    }
+
+    /* 额外处理登录接口 */
+    if (method.meta?.login || method.meta?.authRole === 'login') {
+      const { token, ...data } = result.data ?? {};
+      result.data = {
+        accessToken: token,
+        refreshToken: null,
+        ...data,
+      };
+    }
+    return result;
+  },
+});
+http.interceptor('error', async (error) => {
+  if (error?.code === 401) await useAuthStore().logout();
+  message.error(error.message ?? `服务错误 (${error.code})`).then();
+});
+
+export const database = createRequestClient({
+  baseURL: `${import.meta.env.BASE_URL}database`,
+  transform(body) {
+    return { code: 0, data: body, message: 'ok' } as any;
+  },
+});
+
+export type TransformData<T = any> = Recordable<T>;
+
+export interface TransformList<T = TransformData> {
+  total: number;
+  items: T[];
+  data?: {
+    page: number;
+    size: number;
+    total: number;
+  };
+}
+
+export interface TransformBody<T> {
+  code: number;
+  data: T;
+  message?: string;
+}
+
+export interface TransformBlob {
+  fileName: string;
+  source: Blob;
+}
+
+export interface TransformRecord {
+  id: string;
+  createUser?: string;
+  createTime?: string;
+  updateUser?: string;
+  updateTime?: string;
+  lastTime?: string;
+  lastUser?: string;
+}

+ 86 - 0
apps/smart-pharmacy/src/api/method/access.ts

@@ -0,0 +1,86 @@
+import type { UserInfo } from '@vben/types';
+
+import type { SystemModel, TransformData } from '#/api';
+
+import { http } from '#/api';
+import { fromRole } from '#/api/model';
+import { fromMenus } from '#/api/model/menu';
+
+export namespace AccessModel {
+  export interface LoginParams {
+    password: 'string';
+    username: 'string';
+  }
+
+  export interface LoginResponse {
+    accessToken: string;
+    refreshToken?: string;
+
+    [K: string]: any;
+  }
+}
+
+export function loginMethod(data: AccessModel.LoginParams) {
+  return http.post<AccessModel.LoginResponse>(`/login`, data, {
+    meta: { login: true, visitor: true },
+  });
+}
+
+export function getAccessMenuMethod(permissions?: string[]) {
+  return http.post<SystemModel.Menu[], TransformData[]>(
+    `/admin/menu/allMenu`,
+    void 0,
+    {
+      transform(data) {
+        const menus = fromMenus(data);
+        if (!permissions?.length) return menus;
+        const forEach = (
+          permissions: Set<string>,
+          menus: SystemModel.Menu[],
+          parentMenu: SystemModel.Menu[] = [],
+        ) => {
+          for (const menu of menus) {
+            const id = menu.id;
+            if (permissions.has(id)) {
+              permissions.delete(id);
+              if (menu.type === 'menu') parentMenu.push(menu);
+              else if (menu.type === 'catalog' && menu.children?.length) {
+                const parent = { ...menu, children: [] };
+                parentMenu.push(parent);
+                forEach(permissions, menu.children, parent.children);
+              }
+            } else if (menu.type === 'catalog' && menu.children?.length) {
+              forEach(permissions, menu.children, parentMenu);
+            }
+            if (permissions.size === 0) break;
+          }
+          return parentMenu;
+        };
+        return forEach(new Set(permissions), menus);
+      },
+    },
+  );
+}
+
+export function getUserInfoMethod(token?: string) {
+  return http.get<UserInfo & { roles: SystemModel.Role[] }, TransformData>(
+    `/getInfo`,
+    {
+      headers: { Authorization: token },
+      transform(data, headers) {
+        return {
+          avatar: data?.headImage,
+          realName: '',
+          roles: data?.roles?.map(fromRole) ?? [],
+          userId: data?.userid,
+          username: data?.username,
+          // user
+          token: token ?? headers.get('authorization') ?? '',
+          homePath: '',
+          desc: '',
+        } satisfies UserInfo;
+      },
+    },
+  );
+
+}

+ 182 - 0
apps/smart-pharmacy/src/api/method/business.ts

@@ -0,0 +1,182 @@
+import type {
+  TransformBlob,
+  TransformBody,
+  TransformData,
+  TransformList,
+  TransformRecord,
+} from '#/api';
+
+import { downloadFileFromBlob } from '@vben/utils';
+
+import { http } from '#/api';
+import {
+  fromDepartment,
+  fromDoctor,
+  toDepartment,
+  toDoctor,
+} from '#/api/model';
+
+export namespace BusinessModel {
+  export interface Department extends TransformRecord {
+    id: string;
+    name: string;
+    code?: string;
+    description?: string;
+    organization?: { name: string };
+    parent?: Department;
+    children?: Department[];
+    registerLink?: string;
+  }
+  export interface Doctor extends TransformRecord {
+    id: string;
+    name: string;
+    code?: string;
+    avatar?: string;
+    description?: string;
+    worker?: string;
+    department?: Department;
+    titleOfClinical?: string;
+    titleOfTeach?: string;
+    titleOf?: string;
+    adeptAt?: string;
+    registerLink?: string;
+  }
+}
+
+export function listDepartmentsMethod(
+  page = 1,
+  size = 20,
+  query?: Partial<BusinessModel.Department>,
+) {
+  return http.post<TransformList<BusinessModel.Department>, TransformList>(
+    `/basis/department/listPage`,
+    toDepartment(query),
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return { ...data, items: items.map((item) => fromDepartment(item)) };
+      },
+    },
+  );
+}
+
+export function treeDepartmentsMethod() {
+  return http.post<BusinessModel.Department[], TransformData[]>(
+    `/basis/department/list`,
+    void 0,
+    {
+      transform(data) {
+        return data.map((item) => fromDepartment(item));
+      },
+    },
+  );
+}
+
+export function editDepartmentMethod(data: Partial<BusinessModel.Department>) {
+  return http.post(
+    data?.id ? `/basis/department/update` : `/basis/department/add`,
+    toDepartment(data),
+  );
+}
+
+export function deleteDepartmentMethod(
+  data: Pick<BusinessModel.Department, 'id'>,
+) {
+  return deleteDepartmentsMethod([data]);
+}
+
+export function deleteDepartmentsMethod(
+  params: Pick<BusinessModel.Department, 'id'>[],
+) {
+  return http.post(`/basis/department/batchDelete`, void 0, {
+    params: { ids: params.map((item) => item.id).join(',') },
+  });
+}
+
+export function downloadDepartmentTemplateMethod(filename = '科室模板.xlsx') {
+  return http.get(`/basis/department/downExcel`, {
+    params: { fileName: filename },
+    transform(data: TransformBlob) {
+      downloadFileFromBlob(data);
+      return data;
+    },
+  });
+}
+export function uploadDepartmentFileMethod(file: File) {
+  const data = new FormData();
+  data.append('file', file);
+  return http.post(`/basis/department/importExcel`, data, {
+    meta: { notParseResponseBody: true },
+    transform(data: TransformBody<unknown>) {
+      if (data.code === 0) {
+        const [_, count = ''] = data.message?.match(/入库成功(\d+)条/) ?? [];
+        if (+count > 0)
+          return { count: +count, message: data.message ?? `导入成功` };
+      }
+      // eslint-disable-next-line no-throw-literal
+      throw { message: data.message ?? `导入失败` };
+    },
+  });
+}
+
+export function listDoctorsMethod(
+  page = 1,
+  size = 20,
+  query?: Partial<BusinessModel.Doctor>,
+) {
+  return http.post<TransformList<BusinessModel.Doctor>, TransformList>(
+    `/basis/doctor/listPage`,
+    toDoctor(query),
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return { ...data, items: items.map((item) => fromDoctor(item)) };
+      },
+    },
+  );
+}
+
+export function editDoctorMethod(data: Partial<BusinessModel.Doctor>) {
+  return http.post(
+    data?.id ? `/basis/doctor/update` : `/basis/doctor/add`,
+    toDoctor(data),
+  );
+}
+
+export function deleteDoctorMethod(data: Pick<BusinessModel.Doctor, 'id'>) {
+  return deleteDoctorsMethod([data]);
+}
+
+export function deleteDoctorsMethod(
+  params: Pick<BusinessModel.Doctor, 'id'>[],
+) {
+  return http.post(`/basis/doctor/batchDelete`, void 0, {
+    params: { ids: params.map((item) => item.id).join(',') },
+  });
+}
+
+export function downloadDoctorTemplateMethod(filename = '医生模板.xlsx') {
+  return http.get(`/basis/doctor/downExcel`, {
+    params: { fileName: filename },
+    transform(data: TransformBlob) {
+      downloadFileFromBlob(data);
+      return data;
+    },
+  });
+}
+export function uploadDoctorFileMethod(file: File) {
+  const data = new FormData();
+  data.append('file', file);
+  return http.post(`/basis/doctor/importExcel`, data, {
+    meta: { notParseResponseBody: true },
+    transform(data: TransformBody<unknown>) {
+      if (data.code === 0) {
+        const [_, count = ''] = data.message?.match(/入库成功(\d+)条/) ?? [];
+        if (+count > 0)
+          return { count: +count, message: data.message ?? `导入成功` };
+      }
+      // eslint-disable-next-line no-throw-literal
+      throw { message: data.message ?? `导入失败` };
+    },
+  });
+}

+ 20 - 0
apps/smart-pharmacy/src/api/method/common.ts

@@ -0,0 +1,20 @@
+import type { TransformBody } from '#/api';
+
+import { http } from '#/api';
+
+export function uploadFileMethod(file: File) {
+  const data = new FormData();
+  data.append('file', file);
+  return http.post(`/common/upload`, data, {
+    meta: { notParseResponseBody: true },
+    transform(data: TransformBody<unknown>) {
+      if (data.code === 0 && data.message?.startsWith('http'))
+        return data.message;
+      else if (data.code === 0) {
+        return 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png';
+      }
+      // eslint-disable-next-line no-throw-literal
+      throw { message: data.message ?? `上传失败` };
+    },
+  });
+}

+ 52 - 0
apps/smart-pharmacy/src/api/method/operate.ts

@@ -0,0 +1,52 @@
+import type { TransformData, TransformList, TransformRecord } from '#/api';
+
+import { http } from '#/api';
+import { fromRecord, toRecord } from '#/api/model/operate';
+
+export namespace OperateModel {
+  export interface Record extends TransformRecord {
+    [key: string]: any;
+    id: string;
+    name: string;
+    itemName: string;
+    acupoint: string;
+    description: string;
+    operateUserName: string;
+    operateDate: string;
+    operationRemark: string;
+    photoUrl: string;
+    issueInstitutionName: string;
+    issueDoctorName: string;
+    issueDate: string;
+    treatmentDescription: string;
+    treatmentTime: string;
+    remark?: string;
+    status: 0 | 1;
+    startTime: string;
+    endTime: string;
+  }
+}
+// 获取机构列表
+export function listRecordsMethod(page = 1, size = 20, query?: TransformData) {
+  return http.post<TransformList<OperateModel.Record>, TransformList>(
+    `/basis/operate/listPage`,
+    toRecord(query),
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return { ...data, items: items.map((item) => fromRecord(item)) };
+      },
+    },
+  );
+}
+// 获取记录详情
+export function getRecordDetailMethod(id: string) {
+  return http.Post<OperateModel.Record, TransformData>(
+    `/basis/operate/operateDetail?id=${id}`,
+    {
+      transform(data) {
+        return fromRecord(data);
+      },
+    },
+  );
+}

+ 68 - 0
apps/smart-pharmacy/src/api/method/register.ts

@@ -0,0 +1,68 @@
+import type { TransformData, TransformList, TransformRecord } from '#/api';
+
+import { http } from '#/api';
+import { fromRegister, toRegister } from '#/api/model/register';
+
+export namespace RegisterModel {
+  export interface Register extends TransformRecord {
+    [key: string]: any;
+    id: string;
+    name: string;
+    itemName: string;
+    itemCode: string;
+    itemState: number;
+    totalNum: number;
+    completeNum: number;
+    describe: string;
+    issueDate: string;
+    issueDoctorId: string;
+    issueDoctorName: string;
+    issueInstitutionId: string;
+    issueInstitutionName: string;
+    progressStatus: 0 | 1;
+    finishTime: string;
+    nextDate: string;
+    patientId: string;
+    phone: string;
+    sex: string;
+    writeoff: number;
+  }
+}
+// 获取登记列表
+export function listRegisterMethod(page = 1, size = 20, query?: TransformData) {
+  return http.post<TransformList<RegisterModel.Register>, TransformList>(
+    `/basis/item/registerList`,
+    toRegister(query),
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return {
+          ...data,
+          items: items.map((item) => fromRegister(item)),
+        };
+      },
+    },
+  );
+}
+// 获取可核销项目
+export function getWriteOffListMethod(id: string) {
+  return http.Post<RegisterModel.Register, TransformData>(
+    `/basis/item/writeOffList?id=${id}`,
+    {
+      transform(data) {
+        return fromRegister(data);
+      },
+    },
+  );
+}
+// 核销项目
+export function writeOffItemMethod(ids: string[]) {
+  return http.Post<RegisterModel.Register, TransformData>(
+    `/basis/item/writeOff?ids=${ids}`,
+    {
+      transform(data) {
+        return fromRegister(data);
+      },
+    },
+  );
+}

+ 284 - 0
apps/smart-pharmacy/src/api/method/system.ts

@@ -0,0 +1,284 @@
+import type { RouteMeta } from 'vue-router';
+
+import type { TransformData, TransformList, TransformRecord } from '#/api';
+
+import { http } from '#/api';
+import {
+  fromOrganization,
+  fromProject,
+  fromRole,
+  fromUser,
+  toOrganization,
+  toProject,
+  toRole,
+  toUser,
+} from '#/api/model';
+import { fromMenus } from '#/api/model/menu';
+
+export namespace SystemModel {
+  export interface Project extends TransformRecord {
+    id: string;
+    institutionName?: string;
+    createDate?: string;
+    updateDate?: string;
+    institutionCode?: string;
+    institutionId?: string;
+    itemName: string;
+    itemCode?: string;
+    sourceCode?: string;
+    sourceName?: string;
+  }
+  export interface Role extends TransformRecord {
+    [key: string]: any;
+    id: string;
+    name: string;
+    code?: string;
+    permissions: string[];
+    remark?: string;
+    status: 0 | 1;
+  }
+
+  export interface User extends TransformRecord {
+    id: string;
+    access: string;
+    name: string;
+    worker?: string;
+    mobile?: string;
+    roles?: Array<Role | string>;
+    sititutionId?: string;
+    pid?: string;
+    password?: string;
+    stateSel?: 0 | 1;
+    status?: 0 | 1;
+    roleNames?: string;
+    createUser?: string;
+    hospitalName?: string;
+  }
+
+  export interface Organization extends TransformRecord {
+    id: string;
+    name: string;
+    code?: string;
+    superior?: string;
+    createTime?: string;
+    createUser?: string;
+    parentInstitutionId?: string;
+    parentinstitutionSelsourceName?: string;
+    type?: string;
+    creditCode?: string;
+    medicineCenter?: string;
+    remark?: string;
+  }
+
+  export interface Menu {
+    type: 'button' | 'catalog' | 'menu';
+    id: string;
+    pid?: string;
+    name: string;
+    path: string;
+    component?: string;
+    meta: Partial<RouteMeta>;
+    children?: Menu[];
+  }
+}
+// 获取机构列表
+export function listOrganizationsMethod(
+  page = 1,
+  size = 20,
+  query?: TransformData,
+) {
+  return http.post<TransformList<SystemModel.Organization>, TransformList>(
+    `/basis/medicalinstitutionsMgr/listPain`,
+    toOrganization(query),
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return { ...data, items: items.map((item) => fromOrganization(item)) };
+      },
+    },
+  );
+}
+// 获取项目管理列表
+export function listProjectsMethod(page = 1, size = 20, query?: TransformData) {
+  return http.post<TransformList<SystemModel.Project>, TransformList>(
+    `/basis/institution/listPage`,
+    toProject(query),
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return { ...data, items: items.map((item) => fromProject(item)) };
+      },
+    },
+  );
+}
+
+export function listRolesMethod(page = 1, size = 20, query?: TransformData) {
+  return http.post<TransformList<SystemModel.Role>, TransformList>(
+    `/admin/right_RoleMgr/listPain`,
+    query,
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return { ...data, items: items.map((item) => fromRole(item)) };
+      },
+    },
+  );
+}
+
+export function optionsRoleMethod() {
+  return http.get<SystemModel.Role[], TransformData[]>(
+    `/admin/right_RoleMgr/optionselect`,
+    {
+      transform(data) {
+        return data.map((item) => fromRole(item));
+      },
+    },
+  );
+}
+// 获取来源平台
+export function listSourcePlatformMethod() {
+  return http.Post<SystemModel.Project[], TransformData[]>(
+    `/basis/institution/sourceList`,
+    {
+      transform(data) {
+        return data.map((item) => fromProject(item));
+      },
+    },
+  );
+}
+// 获取全部机构
+export function listOrganizationsMethodAll() {
+  return http.Post<SystemModel.Organization[], TransformData[]>(
+    `/basis/medicalinstitutionsMgr/list`,
+    {
+      transform(data) {
+        return data.map((item) => fromOrganization(item));
+      },
+    },
+  );
+}
+// 获取机构(属树形结构)
+export function listUsersInstitutionMethodTree() {
+  return http.Post<SystemModel.User[], TransformData[]>(
+    `/basis/medicalinstitutionsMgr/treeList`,
+    {
+      transform(data) {
+        return data.map((item) => fromUser(item));
+      },
+    },
+  );
+}
+// 编辑机构
+export function editOrganizationMethod(
+  data: Partial<SystemModel.Organization>,
+) {
+  return http.post(
+    data.id
+      ? `/basis/medicalinstitutionsMgr/update`
+      : `/basis/medicalinstitutionsMgr/Add`,
+    toOrganization(data),
+  );
+}
+export function editRoleMethod(data: Partial<SystemModel.Role>) {
+  return http.post(
+    data.id ? `/admin/right_RoleMgr/update` : `/admin/right_RoleMgr/Add`,
+    toRole(data),
+  );
+}
+
+export function updateRoleStatusMethod(
+  id: string,
+  data: Partial<Omit<SystemModel.Role, 'id'>>,
+) {
+  const { pid, stateSel } = toRole({ ...data, id });
+  return http.put(`/admin/right_RoleMgr/changeStatus`, { pid, stateSel });
+}
+
+export function deleteRoleMethod(data: Pick<SystemModel.User, 'id'>) {
+  return deleteRolesMethod([data]);
+}
+
+export function deleteRolesMethod(params: Pick<SystemModel.User, 'id'>[]) {
+  return http.post(`/admin/right_RoleMgr/BatchDelete`, void 0, {
+    params: { ids: params.map((item) => item.id).join(',') },
+  });
+}
+
+// 获取用户列表
+export function listUsersMethod(page = 1, size = 20, query?: SystemModel.User) {
+  return http.post<TransformList<SystemModel.User>, TransformList>(
+    `/portal/userMgr/listPain`,
+    toUser(query),
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return { ...data, items: items.map((item) => fromUser(item)) };
+      },
+    },
+  );
+}
+// 编辑或新增用户
+// 编辑项目
+export function editProjectMethod(data: Partial<SystemModel.Project>) {
+  return http.post(
+    data.id ? `/basis/institution/update` : `/basis/institution/Add`,
+    toProject(data),
+  );
+}
+
+export function editUserMethod(data: Partial<SystemModel.User>) {
+  return http.post(
+    data?.id ? `/portal/userMgr/update` : `/portal/userMgr/Add`,
+    toUser(data),
+  );
+}
+// 用户状态更改
+export function updateUserStatusMethod(
+  pid: string,
+  { status }: { status: 0 | 1 },
+) {
+  return http.Post(`/portal/userMgr/updateState`, { pid, stateSel: status });
+}
+export function getUserMethod(id: string) {
+  return http.get<SystemModel.User, TransformData>(`/portal/userMgr/${id}`, {
+    transform(data) {
+      return fromUser(data);
+    },
+  });
+}
+
+export function deleteUserMethod(data: Pick<SystemModel.User, 'pid'>) {
+  console.warn('data', data);
+  return deleteUsersMethod([data]);
+}
+
+export function deleteUsersMethod(params: Pick<SystemModel.User, 'pid'>[]) {
+  return http.post(`/portal/userMgr/BatchDelete`, void 0, {
+    params: { ids: params.map((item) => item.pid).join(',') },
+  });
+}
+// 删除机构
+export function deleteOrganizationMethod(
+  data: Pick<SystemModel.Organization, 'id'>,
+) {
+  return deleteOrganizationsMethod([data]);
+}
+export function deleteOrganizationsMethod(
+  params: Pick<SystemModel.Organization, 'id'>[],
+) {
+  return http.post(`/basis/medicalinstitutionsMgr/BatchDelete`, void 0, {
+    params: { ids: params.map((item) => item.id).join(',') },
+  });
+}
+
+export function getMenusMethod() {
+  return http.post<SystemModel.Menu[], TransformData[]>(
+    `/admin/menu/allMenu`,
+    void 0,
+    {
+      transform(data) {
+        return fromMenus(data);
+      },
+    },
+  );
+}

+ 159 - 0
apps/smart-pharmacy/src/api/method/treatment.ts

@@ -0,0 +1,159 @@
+import type { TransformData, TransformList } from '#/api';
+
+import { http } from '#/api';
+import { fromPatient, toPatient } from '#/api/model/patient';
+import { fromTreatmentTab } from '#/api/model/tab';
+import { fromTreatmentDetail } from '#/api/model/treatmentDetail';
+
+export namespace TreatmentModel {
+  export interface TreatmentTab {
+    itemName: string;
+    patientNum: number;
+  }
+  export interface Patient {
+    patientId: string;
+    name: string;
+    sex: string;
+    age: number;
+    phone: string;
+    itemVOS: [
+      {
+        id: string;
+        itemCode: string;
+        itemName: string;
+        itemState: number;
+      },
+    ];
+  }
+  export interface TreatmentDetail {
+    id: string;
+    currentOperate: {
+      id: string;
+      treatmentTime: string;
+    };
+    acupoints: [
+      {
+        acupointCode: string;
+        acupointName: string;
+        acuType: string;
+        id: string;
+        knowledgeId: string;
+      },
+    ];
+    behavior: string;
+    completeNum: number;
+    constitution: string;
+    createTime: string;
+    describe: string;
+    diagnosis: string;
+    finishTime: string;
+    frequency: string;
+    isDeleted: number;
+    issueDate: string;
+    issueDoctorId: string;
+    issueDoctorName: string;
+    issueInstitutionId: string;
+    issueInstitutionName: string;
+    itemCode: string;
+    itemName: string;
+    itemState: number;
+    nextDate: string;
+    operates: [
+      {
+        id: string;
+        isDelete: number;
+        operateDate: string;
+        operateUserId: string;
+        operateUserName: string;
+        recordId: string;
+        remark: string;
+        treatmentImageUrl: string;
+        treatmentTime: string;
+      },
+    ];
+    patientId: string;
+    planCode: string;
+    planType: string;
+    progressStatus: number;
+    totalNum: number;
+    updateTime: string;
+    writeOffDate: string;
+    writeoff: string;
+    treatmentTime: number;
+  }
+}
+
+// 获取未治疗项目统计
+export function listUntreatedProjectsMethod() {
+  return http.Post<TreatmentModel.TreatmentTab, TransformData>(
+    `/basis/treatment/waitTreatmentCount`,
+    {
+      transform(data: any) {
+        return fromTreatmentTab(data);
+      },
+    },
+  );
+}
+// 获取未治疗患者列表
+export function listPatientsMethod(page = 1, size = 20, query?: TransformData) {
+  return http.Post<TransformList<TreatmentModel.Patient>, TransformList>(
+    `/basis/treatment/waitTreatmentList`,
+    toPatient(query),
+    {
+      params: { page, limit: size },
+      transform({ items, ...data }) {
+        return { ...data, items: items.map((item) => fromPatient(item)) };
+      },
+    },
+  );
+}
+// 获取治疗项目详情
+export function getTreatmentDetailMethod(id: string) {
+  return http.Post<TreatmentModel.TreatmentDetail, TransformData>(
+    `/basis/treatment/treatmentDetail?id=${id}`,
+    {
+      transform(data: any) {
+        return fromTreatmentDetail(data);
+      },
+    },
+  );
+}
+// 保存治疗
+export function saveTreatmentMethod(
+  data: Partial<TreatmentModel.TreatmentDetail>,
+) {
+  return http.post(`/basis/treatment/saveStartTreatment`, data);
+}
+// 获取穴位详情
+export function getAcupointDetailMethod(id: string) {
+  console.log(id, '获取穴为id');
+  return http.Post<any, any>(
+    `/knowledge/acuPoint/detail`,
+    { id },
+    {
+      // meta: { notParseResponseBody: true },
+      transform(data: any) {
+        return data;
+      },
+    },
+  );
+}
+// 获取经络详情
+export function getMeridianDetailMethod(id: string) {
+  return http.Post<any, TransformData>(
+    `/knowledge/acuMeridian/detail`,
+    { id },
+    {
+      // meta: { notParseResponseBody: true },
+      transform(data: any) {
+        return data;
+      },
+    },
+  );
+}
+// 治疗结束
+export function endTreatmentMethod(ids: string[]) {
+  return http.Post<any, TransformData>(
+    `/basis/treatment/endTreatment?ids=${ids}`,
+  );
+}

+ 43 - 0
apps/smart-pharmacy/src/api/model/department.ts

@@ -0,0 +1,43 @@
+import type { BusinessModel, TransformData } from '#/api';
+
+import { fromRow } from '#/api/model';
+
+export function fromDepartment(data?: TransformData): BusinessModel.Department {
+  return {
+    ...fromRow(data),
+    id: data?.pid,
+    name: data?.deptName,
+    code: data?.deptCode,
+    description: data?.introduce,
+    organization: data?.hospitalName
+      ? <any>{
+          code: data?.hospitalCode,
+          name: data?.hospitalName,
+        }
+      : void 0,
+    parent: data?.parentDeptName
+      ? <BusinessModel.Department>{
+          code: data?.parentDeptCode,
+          name: data?.parentDeptName,
+        }
+      : void 0,
+    children:
+      data?.children?.map((item: TransformData) => fromDepartment(item)) ?? [],
+    registerLink: data?.registerUrl,
+  };
+}
+
+export function toDepartment(
+  data?: Partial<BusinessModel.Department>,
+): TransformData {
+  return {
+    pid: data?.id,
+    deptName: data?.name,
+    deptCode: data?.code,
+    introduce: data?.description,
+    hospitalName: data?.organization?.name,
+    parentDeptCode: data?.parent?.code,
+    parentDeptName: data?.parent?.name,
+    registerUrl: data?.registerLink,
+  };
+}

+ 46 - 0
apps/smart-pharmacy/src/api/model/doctor.ts

@@ -0,0 +1,46 @@
+import type { BusinessModel, TransformData } from '#/api';
+
+import { fromRow } from '#/api/model';
+
+export function fromDoctor(data?: TransformData): BusinessModel.Doctor {
+  return {
+    ...fromRow(data),
+    id: data?.id,
+    name: data?.doctorName,
+    description: data?.introduce,
+    titleOf: data?.title,
+    titleOfClinical: data?.clinicalTitle,
+    titleOfTeach: data?.teachTitle,
+    avatar: data?.headImage,
+    worker: data?.doctorCode,
+    adeptAt: data?.expertiseArea,
+    department: data?.deptName
+      ? <BusinessModel.Department>{
+          code: data?.deptCode,
+          name: data?.deptName,
+        }
+      : void 0,
+    registerLink: data?.registerUrl,
+  };
+}
+
+export function toDoctor(data?: Partial<BusinessModel.Doctor>): TransformData {
+  return {
+    id: data?.id,
+    doctorName: data?.name,
+    doctorCode: data?.worker,
+    headImage: data?.avatar,
+
+    title: data?.titleOf,
+    clinicalTitle: data?.titleOfClinical,
+    teachTitle: data?.titleOfTeach,
+
+    deptCode: data?.department?.code,
+    deptName: data?.department?.name,
+
+    expertiseArea: data?.adeptAt,
+    introduce: data?.description,
+
+    registerUrl: data?.registerLink,
+  };
+}

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

@@ -0,0 +1,23 @@
+import type { TransformData, TransformRecord } from '#/api';
+
+export * from './department';
+export * from './doctor';
+export * from './organization';
+export * from './project';
+export * from './role';
+export * from './user';
+export function fromRow(data?: TransformData): TransformRecord {
+  const createUser = data?.createUser;
+  const createTime = data?.createTime ?? data?.createDate;
+  const updateUser = data?.updateUser;
+  const updateTime = data?.updateTime ?? data?.updateDate;
+  return {
+    id: data?.id,
+    createUser,
+    createTime,
+    updateUser,
+    updateTime,
+    lastTime: updateTime || createTime,
+    lastUser: updateUser || createUser,
+  };
+}

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

@@ -0,0 +1,49 @@
+import type { SystemModel, TransformData } from '#/api';
+
+export function fromMenus(menus: TransformData[]): SystemModel.Menu[] {
+  const getType = (menu: TransformData): SystemModel.Menu['type'] => {
+    if (menu.type) return menu.type;
+    if (menu.component && menu.children === null) return 'menu';
+    return menu.component ? 'catalog' : 'button';
+  };
+  return Array.isArray(menus)
+    ? menus
+        .map((menu: TransformData) => {
+          menu.meta ??= {};
+          // 使用后端的 orderNum 优先;其可能为字符串,这里转为数字;没有则回退到已有 meta.order;仍无则给很大默认值
+          const computedOrder = Number(
+            menu?.orderNum ?? (menu.meta as any)?.order ?? 999_999,
+          );
+          menu.meta.order = Number.isFinite(computedOrder)
+            ? computedOrder
+            : 999_999;
+          return {
+            type: getType(menu),
+            id: menu.id ?? menu.meta.id,
+            pid: menu.parentId,
+            name: menu.name,
+            path: menu.path,
+            component: menu.component,
+            meta: fromMenuMeta(menu.meta),
+            children: fromMenus(menu.children),
+          } satisfies SystemModel.Menu;
+        })
+        // 未设置排序的项默认放到最后
+        .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999))
+    : [];
+}
+
+type MenuMeta = SystemModel.Menu['meta'];
+
+export function getDefaultMenuMeta(meta?: MenuMeta): MenuMeta {
+  return Object.assign(
+    {
+      keepAlive: true,
+    },
+    meta,
+  );
+}
+
+function fromMenuMeta(meta: TransformData): MenuMeta {
+  return getDefaultMenuMeta(meta satisfies MenuMeta);
+}

+ 49 - 0
apps/smart-pharmacy/src/api/model/operate.ts

@@ -0,0 +1,49 @@
+import type { TransformData } from '#/api';
+import type { OperateModel } from '#/api/method/operate';
+
+import { fromRow } from '#/api/model';
+
+export function fromRecord(data?: TransformData): OperateModel.Record {
+  return {
+    ...fromRow(data),
+    id: data?.id,
+    recordId: data?.recordId,
+    name: data?.name,
+    itemName: data?.itemName,
+    acupointName: data?.acupointName,
+    describe: data?.describe,
+    operateUserName: data?.operateUserName,
+    operateDate: data?.operateDate,
+    treatmentImageUrl: data?.treatmentImageUrl,
+    treatmentTime: data?.treatmentTime,
+    issueInstitutionName: data?.issueInstitutionName,
+    issueDoctorName: data?.issueDoctorName,
+    issueDate: data?.issueDate,
+    remark: data?.remark,
+    status: data?.status,
+    startTime: data?.startTime,
+    endTime: data?.endTime,
+  };
+}
+
+export function toRecord(data?: Partial<OperateModel.Record>): TransformData {
+  return {
+    id: data?.id,
+    recordId: data?.recordId,
+    name: data?.name,
+    itemName: data?.itemName,
+    describe: data?.describe,
+    operateUserName: data?.operateUserName,
+    operateDate: data?.operateDate,
+    treatmentImageUrl: data?.treatmentImageUrl,
+    issueInstitutionName: data?.issueInstitutionName,
+    issueDoctorName: data?.issueDoctorName,
+    issueDate: data?.issueDate,
+    treatmentTime: data?.treatmentTime,
+    remark: data?.remark,
+    status: data?.status,
+    acupointName: data?.acupointName,
+    startTime: data?.startTime,
+    endTime: data?.endTime,
+  };
+}

+ 40 - 0
apps/smart-pharmacy/src/api/model/organization.ts

@@ -0,0 +1,40 @@
+import type { SystemModel, TransformData } from '#/api';
+
+import { fromRow } from '#/api/model';
+
+export function fromOrganization(
+  data?: TransformData,
+): SystemModel.Organization {
+  return {
+    ...fromRow(data),
+    id: data?.pid,
+    name: data?.name,
+    code: data?.code,
+    type: data?.type,
+    parentInstitutionId: data?.parentInstitutionId,
+    parentinstitutionSelsourceName: data?.parentinstitutionSelsourceName,
+    creditCode: data?.creditCode,
+    medicineCenter: data?.medicineCenter,
+    remark: data?.remark,
+    createTime: data?.createTime,
+    createUser: data?.createUser,
+  };
+}
+
+export function toOrganization(
+  data?: Partial<SystemModel.Organization>,
+): TransformData {
+  return {
+    pid: data?.id,
+    name: data?.name,
+    code: data?.code,
+    type: data?.type,
+    parentInstitutionId: data?.parentInstitutionId,
+    parentinstitutionSelsourceName: data?.parentinstitutionSelsourceName,
+    creditCode: data?.creditCode,
+    medicineCenter: data?.medicineCenter,
+    remark: data?.remark,
+    createTime: data?.createTime,
+    createUser: data?.createUser,
+  };
+}

+ 36 - 0
apps/smart-pharmacy/src/api/model/patient.ts

@@ -0,0 +1,36 @@
+import type { TransformData } from '#/api';
+import type { TreatmentModel } from '#/api/method/treatment';
+
+export function fromPatient(data?: TransformData): TreatmentModel.Patient {
+  return {
+    patientId: data?.patientId,
+    name: data?.name,
+    sex: data?.sex,
+    age: data?.age,
+    phone: data?.phone,
+    itemVOS: data?.itemVOS?.map((item: any) => ({
+      id: item?.id,
+      itemCode: item?.itemCode,
+      itemName: item?.itemName,
+      itemState: item?.itemState,
+    })),
+  };
+}
+
+export function toPatient(
+  data?: Partial<TreatmentModel.Patient>,
+): TransformData {
+  return {
+    patientId: data?.patientId,
+    name: data?.name,
+    sex: data?.sex,
+    age: data?.age,
+    phone: data?.phone,
+    itemVOS: data?.itemVOS?.map((item: any) => ({
+      id: item?.id,
+      itemCode: item?.itemCode,
+      itemName: item?.itemName,
+      itemState: item?.itemState,
+    })),
+  };
+}

+ 32 - 0
apps/smart-pharmacy/src/api/model/project.ts

@@ -0,0 +1,32 @@
+import type { SystemModel, TransformData } from '#/api';
+
+import { fromRow } from '#/api/model';
+
+export function fromProject(data?: TransformData): SystemModel.Project {
+  return {
+    ...fromRow(data),
+    id: data?.id,
+    itemName: data?.itemName,
+    sourceName: data?.sourceName,
+    createDate: data?.createDate,
+    updateDate: data?.updateDate,
+    institutionName: data?.institutionName,
+    institutionId: data?.institutionId,
+    institutionCode: data?.institutionCode,
+    sourceCode: data?.sourceCode,
+  };
+}
+
+export function toProject(data?: Partial<SystemModel.Project>): TransformData {
+  return {
+    pid: data?.id,
+    itemName: data?.itemName,
+    sourceName: data?.sourceName,
+    createDate: data?.createDate,
+    updateDate: data?.updateDate,
+    institutionName: data?.institutionName,
+    institutionId: data?.institutionId,
+    institutionCode: data?.institutionCode,
+    sourceCode: data?.sourceCode,
+  };
+}

+ 59 - 0
apps/smart-pharmacy/src/api/model/register.ts

@@ -0,0 +1,59 @@
+import type { TransformData } from '#/api';
+import type { RegisterModel } from '#/api/method/register';
+
+import { fromRow } from '#/api/model';
+
+export function fromRegister(data?: TransformData): RegisterModel.Register {
+  return {
+    ...fromRow(data),
+    id: data?.id,
+    name: data?.name,
+    itemName: data?.itemName,
+    itemCode: data?.itemCode,
+    itemState: data?.itemState,
+    totalNum: data?.totalNum,
+    completeNum: data?.completeNum,
+    describe: data?.describe,
+    issueDate: data?.issueDate,
+    issueDoctorId: data?.issueDoctorId,
+    issueDoctorName: data?.issueDoctorName,
+    issueInstitutionId: data?.issueInstitutionId,
+    issueInstitutionName: data?.issueInstitutionName,
+    progressStatus: data?.progressStatus,
+    finishTime: data?.finishTime,
+    nextDate: data?.nextDate,
+    patientId: data?.patientId,
+    phone: data?.phone,
+    sex: data?.sex,
+    age: data?.age,
+    writeoff: data?.writeoff,
+  };
+}
+
+export function toRegister(
+  data?: Partial<RegisterModel.Register>,
+): TransformData {
+  return {
+    id: data?.id,
+    name: data?.name,
+    itemName: data?.itemName,
+    itemCode: data?.itemCode,
+    itemState: data?.itemState,
+    totalNum: data?.totalNum,
+    completeNum: data?.completeNum,
+    describe: data?.describe,
+    issueDate: data?.issueDate,
+    issueDoctorId: data?.issueDoctorId,
+    issueDoctorName: data?.issueDoctorName,
+    issueInstitutionId: data?.issueInstitutionId,
+    issueInstitutionName: data?.issueInstitutionName,
+    progressStatus: data?.progressStatus,
+    finishTime: data?.finishTime,
+    nextDate: data?.nextDate,
+    patientId: data?.patientId,
+    phone: data?.phone,
+    sex: data?.sex,
+    age: data?.age,
+    writeoff: data?.writeoff,
+  };
+}

+ 25 - 0
apps/smart-pharmacy/src/api/model/role.ts

@@ -0,0 +1,25 @@
+import type { SystemModel, TransformData } from '#/api';
+
+import { fromRow } from '#/api/model/index';
+
+export function fromRole(data?: TransformData): SystemModel.Role {
+  return {
+    ...fromRow(data),
+    id: data?.pid,
+    name: data?.rolename,
+    code: data?.rolecode,
+    remark: data?.remark,
+    status: ({ '0': 1, '1': 0 } as const)[<string>data?.stateSel ?? 1] ?? 1,
+    permissions: Array.isArray(data?.menuIds) ? data?.menuIds : [],
+  };
+}
+
+export function toRole(data?: Partial<SystemModel.Role>): TransformData {
+  return {
+    pid: data?.id,
+    rolename: data?.name,
+    remark: data?.remark,
+    stateSel: data?.status === 0 ? '1' : '0',
+    menuIds: data?.permissions ?? [],
+  };
+}

+ 23 - 0
apps/smart-pharmacy/src/api/model/tab.ts

@@ -0,0 +1,23 @@
+import type { TransformData } from '#/api';
+import type { TreatmentModel } from '#/api/method/treatment';
+
+import { fromRow } from '#/api/model';
+
+export function fromTreatmentTab(
+  data?: TransformData,
+): TreatmentModel.TreatmentTab {
+  return {
+    ...fromRow(data),
+    itemName: data?.itemName,
+    patientNum: data?.patientNum,
+  };
+}
+
+export function toTreatmentTab(
+  data?: Partial<TreatmentModel.TreatmentTab>,
+): TransformData {
+  return {
+    itemName: data?.itemName,
+    patientNum: data?.patientNum,
+  };
+}

+ 74 - 0
apps/smart-pharmacy/src/api/model/treatmentDetail.ts

@@ -0,0 +1,74 @@
+import type { TransformData } from '#/api';
+import type { TreatmentModel } from '#/api/method/treatment';
+
+export function fromTreatmentDetail(
+  data?: TransformData,
+): TreatmentModel.TreatmentDetail {
+  return {
+    id: data?.id,
+    acupoints: data?.acupoints,
+    behavior: data?.behavior,
+    completeNum: data?.completeNum,
+    constitution: data?.constitution,
+    createTime: data?.createTime,
+    describe: data?.describe,
+    diagnosis: data?.diagnosis,
+    finishTime: data?.finishTime,
+    isDeleted: data?.isDeleted,
+    issueDate: data?.issueDate,
+    issueDoctorId: data?.issueDoctorId,
+    issueDoctorName: data?.issueDoctorName,
+    issueInstitutionId: data?.issueInstitutionId,
+    issueInstitutionName: data?.issueInstitutionName,
+    frequency: data?.frequency,
+    itemCode: data?.itemCode,
+    itemName: data?.itemName,
+    itemState: data?.itemState,
+    nextDate: data?.nextDate,
+    operates: data?.operates,
+    patientId: data?.patientId,
+    planCode: data?.planCode,
+    planType: data?.planType,
+    progressStatus: data?.progressStatus,
+    totalNum: data?.totalNum,
+    updateTime: data?.updateTime,
+    writeOffDate: data?.writeOffDate,
+    writeoff: data?.writeoff,
+  };
+}
+
+export function toTreatmentDetail(
+  data?: Partial<TreatmentModel.TreatmentDetail>,
+): TransformData {
+  return {
+    id: data?.id,
+    acupoints: data?.acupoints,
+    behavior: data?.behavior,
+    completeNum: data?.completeNum,
+    constitution: data?.constitution,
+    createTime: data?.createTime,
+    describe: data?.describe,
+    diagnosis: data?.diagnosis,
+    frequency: data?.frequency,
+    itemCode: data?.itemCode,
+    itemName: data?.itemName,
+    itemState: data?.itemState,
+    finishTime: data?.finishTime,
+    isDeleted: data?.isDeleted,
+    issueDate: data?.issueDate,
+    issueDoctorId: data?.issueDoctorId,
+    issueDoctorName: data?.issueDoctorName,
+    issueInstitutionId: data?.issueInstitutionId,
+    issueInstitutionName: data?.issueInstitutionName,
+    nextDate: data?.nextDate,
+    operates: data?.operates,
+    patientId: data?.patientId,
+    planCode: data?.planCode,
+    planType: data?.planType,
+    progressStatus: data?.progressStatus,
+    totalNum: data?.totalNum,
+    updateTime: data?.updateTime,
+    writeOffDate: data?.writeOffDate,
+    writeoff: data?.writeoff,
+  };
+}

+ 44 - 0
apps/smart-pharmacy/src/api/model/user.ts

@@ -0,0 +1,44 @@
+import type { SystemModel, TransformData } from '#/api';
+
+import { fromRole, fromRow, toRole } from '#/api/model';
+
+export function fromUser(data?: TransformData): SystemModel.User {
+  return {
+    ...fromRow(data),
+    id: data?.id,
+    access: data?.userid,
+    name: data?.username,
+    worker: data?.jobnumber,
+    mobile: data?.mobile,
+    pid: data?.pid,
+    roles: data?.roles?.map((item: TransformData) => fromRole(item)) ?? [],
+    sititutionId: data?.sititutionId,
+    status: data?.stateSel === 0 || data?.stateSel === '0' ? 0 : 1,
+    hospitalName: data?.hospitalName,
+    roleNames:
+      data?.roles?.map((item: TransformData) => item.rolename).join(',') ?? '',
+    createUser: data?.createUser,
+  };
+}
+
+export function toUser(data?: Partial<SystemModel.User>): TransformData {
+  const roles =
+    data?.roles?.map((item) =>
+      typeof item === 'string' ? { pid: item } : toRole(item),
+    ) ?? [];
+  return {
+    pid: data?.pid,
+    userid: data?.access,
+    username: data?.name,
+    password: data?.password,
+    jobnumber: data?.worker,
+    mobile: data?.mobile,
+    hospitalName: data?.hospitalName,
+    roles: roles.length > 0 ? roles : void 0,
+    roleIds: roles.map((item) => item.pid).join(',') || void 0,
+    sititutionId: data?.sititutionId,
+    // 查询时:当 status 为空/undefined 时传 null;创建/编辑时为 0/1 则直传
+    stateSel:
+      data?.status === 0 || data?.status === 1 ? (data.status as 0 | 1) : null,
+  };
+}

+ 39 - 0
apps/smart-pharmacy/src/app.vue

@@ -0,0 +1,39 @@
+<script lang="ts" setup>
+import { computed } from 'vue';
+
+import { useAntdDesignTokens } from '@vben/hooks';
+import { preferences, usePreferences } from '@vben/preferences';
+
+import { App, ConfigProvider, theme } from 'ant-design-vue';
+
+import { antdLocale } from '#/locales';
+
+defineOptions({ name: 'App' });
+
+const { isDark } = usePreferences();
+const { tokens } = useAntdDesignTokens();
+
+const tokenTheme = computed(() => {
+  const algorithm = isDark.value
+    ? [theme.darkAlgorithm]
+    : [theme.defaultAlgorithm];
+
+  // antd 紧凑模式算法
+  if (preferences.app.compact) {
+    algorithm.push(theme.compactAlgorithm);
+  }
+
+  return {
+    algorithm,
+    token: tokens,
+  };
+});
+</script>
+
+<template>
+  <ConfigProvider :locale="antdLocale" :theme="tokenTheme">
+    <App>
+      <RouterView />
+    </App>
+  </ConfigProvider>
+</template>

+ 77 - 0
apps/smart-pharmacy/src/bootstrap.ts

@@ -0,0 +1,77 @@
+import { createApp, watchEffect } from 'vue';
+
+import { registerAccessDirective } from '@vben/access';
+import { registerLoadingDirective } from '@vben/common-ui/es/loading';
+import { preferences } from '@vben/preferences';
+import { initStores } from '@vben/stores';
+import '@vben/styles';
+import '@vben/styles/antd';
+
+import { useTitle } from '@vueuse/core';
+
+import { $t, setupI18n } from '#/locales';
+
+import { initComponentAdapter } from './adapter/component';
+import { initSetupVbenForm } from './adapter/form';
+import App from './app.vue';
+// import { router } from './router';
+
+async function bootstrap(namespace: string) {
+  // 初始化组件适配器
+  await initComponentAdapter();
+
+  // 初始化表单组件
+  await initSetupVbenForm();
+
+  // // 设置弹窗的默认配置
+  // setDefaultModalProps({
+  //   fullscreenButton: false,
+  // });
+  // // 设置抽屉的默认配置
+  // setDefaultDrawerProps({
+  //   zIndex: 1020,
+  // });
+
+  const app = createApp(App);
+
+  // 注册v-loading指令
+  registerLoadingDirective(app, {
+    loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
+    spinning: 'spinning',
+  });
+
+  // 国际化 i18n 配置
+  await setupI18n(app);
+
+  // 配置 pinia-tore
+  await initStores(app, { namespace });
+
+  // 安装权限指令
+  registerAccessDirective(app);
+
+  // 初始化 tippy
+  const { initTippy } = await import('@vben/common-ui/es/tippy');
+  initTippy(app);
+
+  // 配置路由及路由守卫
+  const { router } = await import('./router');
+  app.use(router);
+
+  // 配置Motion插件
+  const { MotionPlugin } = await import('@vben/plugins/motion');
+  app.use(MotionPlugin);
+
+  // 动态更新标题
+  watchEffect(() => {
+    if (preferences.app.dynamicTitle) {
+      const routeTitle = router.currentRoute.value.meta?.title;
+      const pageTitle =
+        (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
+      useTitle(pageTitle);
+    }
+  });
+
+  app.mount('#app');
+}
+
+export { bootstrap };

+ 102 - 0
apps/smart-pharmacy/src/components/import/database.modal.vue

@@ -0,0 +1,102 @@
+<script setup lang="ts">
+import type { UploadChangeParam } from 'ant-design-vue';
+
+import { shallowRef, triggerRef } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { FilePlus } from '@vben/icons';
+import { $t } from '@vben/locales';
+
+import { message, UploadDragger } from 'ant-design-vue';
+
+export interface ImportDatabaseProps<U = { count: number; message: string }> {
+  accept?: string;
+  upload?: (file: File) => Promise<U>;
+}
+
+defineOptions({
+  name: 'ImportDatabaseModal',
+});
+
+const emit = defineEmits(['success']);
+
+const props = shallowRef<ImportDatabaseProps>({
+  accept: '*/*',
+});
+
+const [Modal, modalApi] = useVbenModal({
+  showConfirmButton: false,
+  async onConfirm() {},
+  onOpenChange(isOpen) {
+    if (isOpen) {
+      const data = modalApi.getData<ImportDatabaseProps>();
+      Object.assign(props.value, data);
+      triggerRef(props);
+    }
+  },
+});
+
+const onAcceptError = (_files: File[]) => {
+  message.warn(`请上传正确格式的文件`);
+};
+
+async function onChangeHandle(info: UploadChangeParam<File>) {
+  const accept = props.value.accept ?? '*/*';
+  if (info.file.type && accept !== '*/*' && !accept.includes(info.file.type))
+    return onAcceptError(info.fileList);
+
+  modalApi.lock();
+  try {
+    const data = await props.value.upload?.(info.file);
+    if (data?.count) {
+      message.success(data.message);
+      await modalApi.close();
+      emit('success');
+    }
+  } catch (error: any) {
+    message.error(error.message);
+  } finally {
+    modalApi.lock(false);
+  }
+}
+</script>
+
+<template>
+  <Modal
+    class="import-database-modal-wrapper"
+    :draggable="true"
+    :fullscreen-button="false"
+    :confirm-text="$t('ui.actionTitle.import')"
+  >
+    <UploadDragger
+      class="size-full"
+      name="file"
+      :show-upload-list="false"
+      :accept="props.accept"
+      :before-upload="() => false"
+      @change="onChangeHandle"
+      @reject="onAcceptError"
+    >
+      <p class="ant-upload-drag-icon text-center">
+        <FilePlus class="text-primary inline size-24" />
+      </p>
+      <span class="ant-upload-text">单击或将文件拖到此区域进行上传</span>
+    </UploadDragger>
+    <template v-if="$slots['prepend-footer']" #prepend-footer>
+      <slot name="prepend-footer"></slot>
+    </template>
+  </Modal>
+</template>
+
+<style scoped lang="scss">
+:deep(.ant-upload-btn) {
+  display: flex !important;
+  flex-direction: column;
+  justify-content: center;
+  padding: 0 !important;
+}
+
+:global(.import-database-modal-wrapper) {
+  height: 520px;
+}
+</style>

+ 84 - 0
apps/smart-pharmacy/src/core/authentication/login.vue

@@ -0,0 +1,84 @@
+<script lang="ts" setup>
+import type { VbenFormSchema } from '@vben/common-ui';
+
+import type { Recordable } from '@vben-core/typings';
+
+import { computed, markRaw, ref, useTemplateRef } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
+import { $t } from '@vben/locales';
+
+import { message } from 'ant-design-vue';
+
+import { useAuthStore } from '#/store';
+
+defineOptions({ name: 'Login' });
+
+const authStore = useAuthStore();
+
+const formSchema = computed((): VbenFormSchema[] => {
+  return [
+    {
+      component: 'VbenInput',
+      componentProps: {
+        placeholder: $t('authentication.usernameTip'),
+      },
+      fieldName: 'username',
+      label: $t('authentication.username'),
+      rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
+    },
+    {
+      component: 'VbenInputPassword',
+      componentProps: {
+        placeholder: $t('authentication.password'),
+      },
+      fieldName: 'password',
+      label: $t('authentication.password'),
+      rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
+    },
+    {
+      component: markRaw(SliderCaptcha),
+      fieldName: 'captcha',
+      rules: z.boolean().refine((value) => value, {
+        message: $t('authentication.verifyRequiredTip'),
+      }),
+    },
+  ];
+});
+
+const router = useRouter();
+
+const loginRef = useTemplateRef('loginRef');
+const loading = ref(false);
+async function handle(data: Recordable<any>) {
+  loading.value = true;
+  try {
+    await authStore.login(data as any);
+  } catch (error: any) {
+    message.error(error?.message ?? `登录失败`);
+    const formApi = loginRef.value?.getFormApi();
+    formApi?.setFieldValue('captcha', false, false);
+    formApi
+      ?.getFieldComponentRef<InstanceType<typeof SliderCaptcha>>('captcha')
+      ?.resume();
+  } finally {
+    loading.value = false;
+  }
+}
+</script>
+
+<template>
+  <AuthenticationLogin
+    ref="loginRef"
+    :form-schema="formSchema"
+    :loading="loading"
+    @submit="handle"
+    :show-remember-me="false"
+    :show-forget-password="false"
+    :show-code-login="false"
+    :show-qrcode-login="false"
+    :show-third-party-login="false"
+    :show-register="false"
+  />
+</template>

+ 7 - 0
apps/smart-pharmacy/src/core/fallback/coming-soon.vue

@@ -0,0 +1,7 @@
+<script lang="ts" setup>
+import { Fallback } from '@vben/common-ui';
+</script>
+
+<template>
+  <Fallback status="coming-soon" />
+</template>

+ 9 - 0
apps/smart-pharmacy/src/core/fallback/forbidden.vue

@@ -0,0 +1,9 @@
+<script lang="ts" setup>
+import { Fallback } from '@vben/common-ui';
+
+defineOptions({ name: 'Fallback403Demo' });
+</script>
+
+<template>
+  <Fallback status="403" />
+</template>

+ 9 - 0
apps/smart-pharmacy/src/core/fallback/internal-error.vue

@@ -0,0 +1,9 @@
+<script lang="ts" setup>
+import { Fallback } from '@vben/common-ui';
+
+defineOptions({ name: 'Fallback500Demo' });
+</script>
+
+<template>
+  <Fallback status="500" />
+</template>

+ 11 - 0
apps/smart-pharmacy/src/core/fallback/not-found.vue

@@ -0,0 +1,11 @@
+<script lang="ts" setup>
+import { Fallback } from '@vben/common-ui';
+import { useUserStore } from '@vben/stores';
+
+defineOptions({ name: 'Fallback404Demo' });
+const userStore = useUserStore();
+</script>
+
+<template>
+  <Fallback status="404" :home-path="userStore.userInfo?.homePath" />
+</template>

+ 9 - 0
apps/smart-pharmacy/src/core/fallback/offline.vue

@@ -0,0 +1,9 @@
+<script lang="ts" setup>
+import { Fallback } from '@vben/common-ui';
+
+defineOptions({ name: 'FallbackOfflineDemo' });
+</script>
+
+<template>
+  <Fallback status="offline" />
+</template>

+ 26 - 0
apps/smart-pharmacy/src/layouts/auth.vue

@@ -0,0 +1,26 @@
+<script lang="ts" setup>
+import { computed } from 'vue';
+
+import { AuthPageLayout } from '@vben/layouts';
+import { preferences } from '@vben/preferences';
+
+import { $t } from '#/locales';
+
+const appName = computed(() => preferences.app.name);
+const logo = computed(() =>
+  preferences.logo.enable ? preferences.logo.source : void 0,
+);
+</script>
+
+<template>
+  <AuthPageLayout
+    :app-name="appName"
+    :logo="logo"
+    :page-description="$t('authentication.pageDesc')"
+    :page-title="$t('authentication.pageTitle')"
+    :toolbar-list="['color', 'theme']"
+  >
+    <!-- 自定义工具栏 -->
+    <!--<template #toolbar></template>-->
+  </AuthPageLayout>
+</template>

+ 105 - 0
apps/smart-pharmacy/src/layouts/basic.vue

@@ -0,0 +1,105 @@
+<script lang="ts" setup>
+import type { NotificationItem } from '@vben/layouts';
+
+import { computed, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
+import { useWatermark } from '@vben/hooks';
+import { BasicLayout, LockScreen, UserDropdown } from '@vben/layouts';
+import { preferences } from '@vben/preferences';
+import { useAccessStore, useUserStore } from '@vben/stores';
+
+import LoginForm from '#/core/authentication/login.vue';
+import { $t } from '#/locales';
+import { useAuthStore } from '#/store';
+
+const notifications = ref<NotificationItem[]>([]);
+
+const userStore = useUserStore();
+const authStore = useAuthStore();
+const accessStore = useAccessStore();
+const router = useRouter();
+const { destroyWatermark, updateWatermark } = useWatermark();
+
+computed(() => notifications.value.some((item) => !item.isRead));
+
+const avatar = computed(() => {
+  return userStore.userInfo?.avatar || preferences.app.defaultAvatar;
+});
+const text = computed(() => {
+  return (
+    userStore.userInfo?.realName ||
+    userStore.userInfo?.userName ||
+    userStore.userInfo?.username
+  );
+});
+
+async function handleLogout() {
+  await authStore.logout(false);
+}
+
+const dropdownMenus = computed(() => [
+  {
+    text: $t('system.title'),
+    icon: 'ion:settings-outline',
+    handler: () => router.push('/system'),
+  },
+  {
+    text: $t('system.role.title'),
+    icon: 'mdi:account-group',
+    handler: () => router.push('/system/role'),
+  },
+  {
+    text: $t('system.user.title'),
+    icon: 'charm:organisation',
+    handler: () => router.push('/system/user'),
+  },
+  {
+    text: $t('system.organization.title'),
+    icon: 'charm:organisation',
+    handler: () => router.push('/system/organization'),
+  },
+]);
+
+watch(
+  () => preferences.app.watermark,
+  async (enable) => {
+    if (enable) {
+      await updateWatermark({
+        content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
+      });
+    } else {
+      destroyWatermark();
+    }
+  },
+  {
+    immediate: true,
+  },
+);
+</script>
+
+<template>
+  <BasicLayout @clear-preferences-and-logout="handleLogout">
+    <template #user-dropdown>
+      <UserDropdown
+        :avatar
+        :text
+        :menus="dropdownMenus"
+        :description="userStore.userInfo?.desc"
+        @logout="handleLogout"
+      />
+    </template>
+    <template #extra>
+      <AuthenticationLoginExpiredModal
+        v-model:open="accessStore.loginExpired"
+        :avatar
+      >
+        <LoginForm />
+      </AuthenticationLoginExpiredModal>
+    </template>
+    <template #lock-screen>
+      <LockScreen :avatar @to-login="handleLogout" />
+    </template>
+  </BasicLayout>
+</template>

+ 6 - 0
apps/smart-pharmacy/src/layouts/index.ts

@@ -0,0 +1,6 @@
+const BasicLayout = () => import('./basic.vue');
+const AuthPageLayout = () => import('./auth.vue');
+
+const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
+
+export { AuthPageLayout, BasicLayout, IFrameView };

+ 102 - 0
apps/smart-pharmacy/src/locales/index.ts

@@ -0,0 +1,102 @@
+import type { Locale } from 'ant-design-vue/es/locale';
+
+import type { App } from 'vue';
+
+import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
+
+import { ref } from 'vue';
+
+import {
+  $t,
+  setupI18n as coreSetup,
+  loadLocalesMapFromDir,
+} from '@vben/locales';
+import { preferences } from '@vben/preferences';
+
+import antdEnLocale from 'ant-design-vue/es/locale/en_US';
+import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
+import dayjs from 'dayjs';
+
+const antdLocale = ref<Locale>(antdDefaultLocale);
+
+const modules = import.meta.glob('./langs/**/*.json');
+
+const localesMap = loadLocalesMapFromDir(
+  /\.\/langs\/([^/]+)\/(.*)\.json$/,
+  modules,
+);
+/**
+ * 加载应用特有的语言包
+ * 这里也可以改造为从服务端获取翻译数据
+ * @param lang
+ */
+async function loadMessages(lang: SupportedLanguagesType) {
+  const [appLocaleMessages] = await Promise.all([
+    localesMap[lang]?.(),
+    loadThirdPartyMessage(lang),
+  ]);
+  return appLocaleMessages?.default;
+}
+
+/**
+ * 加载第三方组件库的语言包
+ * @param lang
+ */
+async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
+  await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
+}
+
+/**
+ * 加载dayjs的语言包
+ * @param lang
+ */
+async function loadDayjsLocale(lang: SupportedLanguagesType) {
+  let locale;
+  switch (lang) {
+    case 'en-US': {
+      locale = await import('dayjs/locale/en');
+      break;
+    }
+    case 'zh-CN': {
+      locale = await import('dayjs/locale/zh-cn');
+      break;
+    }
+    // 默认使用英语
+    default: {
+      locale = await import('dayjs/locale/en');
+    }
+  }
+  if (locale) {
+    dayjs.locale(locale);
+  } else {
+    console.error(`Failed to load dayjs locale for ${lang}`);
+  }
+}
+
+/**
+ * 加载antd的语言包
+ * @param lang
+ */
+async function loadAntdLocale(lang: SupportedLanguagesType) {
+  switch (lang) {
+    case 'en-US': {
+      antdLocale.value = antdEnLocale;
+      break;
+    }
+    case 'zh-CN': {
+      antdLocale.value = antdDefaultLocale;
+      break;
+    }
+  }
+}
+
+async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
+  await coreSetup(app, {
+    defaultLocale: preferences.app.locale,
+    loadMessages,
+    missingWarn: !import.meta.env.PROD,
+    ...options,
+  });
+}
+
+export { $t, antdLocale, setupI18n };

+ 5 - 0
apps/smart-pharmacy/src/locales/langs/zh-CN/authentication.json

@@ -0,0 +1,5 @@
+{
+  "welcomeBack": "欢迎回来",
+  "pageTitle": "智慧药事系统",
+  "pageDesc": "智能、高效、管理"
+}

+ 5 - 0
apps/smart-pharmacy/src/locales/langs/zh-CN/common.json

@@ -0,0 +1,5 @@
+{
+  "status": "状态",
+  "enabled": "已启用",
+  "disabled": "已禁用"
+}

+ 9 - 0
apps/smart-pharmacy/src/locales/langs/zh-CN/page.json

@@ -0,0 +1,9 @@
+{
+  "auth": {
+    "login": "登录",
+    "register": "注册",
+    "codeLogin": "验证码登录",
+    "qrcodeLogin": "二维码登录",
+    "forgetPassword": "忘记密码"
+  }
+}

+ 25 - 0
apps/smart-pharmacy/src/locales/langs/zh-CN/prescription.json

@@ -0,0 +1,25 @@
+{
+  "title": "处方管理",
+  "list": {
+    "_": "处方",
+    "title": "处方列表",
+    "date": "处方日期",
+    "number": "处方号",
+    "medicalInstitution": "医疗机构",
+    "campus": "院区",
+    "patientName": "患者姓名",
+    "decoctionMethod": "煎药方式",
+    "decoctionEnterprise": "煎药企业",
+    "decoctionCenter": "煎药中心",
+    "deliveryMethod": "配送方式",
+    "logisticsCompany": "物流公司",
+    "processStatus": "流程状态",
+    "actions": "操作",
+    "dateRange": "日期范围",
+    "startTime": "请选择开始时间",
+    "endTime": "请选择结束时间"
+  },
+  "detail": {
+    "_": "处方详情"
+  }
+}

+ 68 - 0
apps/smart-pharmacy/src/locales/langs/zh-CN/system.json

@@ -0,0 +1,68 @@
+{
+  "title": "系统管理",
+  "role": {
+    "_": "角色",
+    "title": "角色管理",
+    "list": "角色列表",
+    "name": "角色名称",
+    "code": "角色标识",
+    "status": "是否启用",
+    "enabledStatus": "启用状态",
+    "remark": "备注",
+    "setPermissions": "菜单权限",
+    "dataAuthority": "数据权限",
+    "oneself": "本人",
+    "oneInstitution": "本机构"
+  },
+  "user": {
+    "_": "用户",
+    "title": "用户管理",
+    "list": "用户列表",
+    "access": "系统账号",
+    "name": "姓名",
+    "worker": "工号",
+    "password": "密码",
+    "mobile": "手机号码",
+    "status": "状态",
+    "remark": "备注"
+  },
+  "organization": {
+    "_": "机构",
+    "belong": "所属机构",
+    "phone": "电话号码",
+    "title": "机构管理",
+    "list": "机构列表",
+    "name": "机构名称",
+    "type": "机构类型",
+    "code": "机构代码",
+    "superior": "上级机构",
+    "createTime": "创建时间",
+    "createUser": "创建者",
+    "input": "请输入",
+    "creditCode": "统一社会信用码",
+    "medicineCenter": "签约煎药中心",
+    "remark": "备注"
+  },
+  "enterprise": {
+    "_": "企业",
+    "title": "企业管理",
+    "name": "企业名称",
+    "type": "企业类型",
+    "code": "企业代码",
+    "createTime": "创建时间",
+    "createUser": "创建者",
+    "remark": "备注"
+  },
+  "tisane": {
+    "_": "煎药中心",
+    "title": "煎药中心管理",
+    "name": "煎药中心名称",
+    "type": "煎药中心类型",
+    "code": "煎药中心代码",
+    "createTime": "创建时间",
+    "createUser": "创建者",
+    "remark": "备注",
+    "relatedOrganization": "关联医疗机构",
+    "relatedEnterprise": "关联企业"
+  }
+}

+ 14 - 0
apps/smart-pharmacy/src/locales/langs/zh-CN/table.json

@@ -0,0 +1,14 @@
+{
+  "column": {
+    "seq": "序号",
+    "operation": "操作",
+    "createUser": "创建人",
+    "createTime": "创建时间",
+    "updateUser": "更新时间",
+    "updateTime": "更新人",
+    "lastUser": "最近一次修改人",
+    "lastTime": "最近一次修改时间",
+    "roleCode": "角色编码",
+    "remark": "备注"
+  }
+}

+ 31 - 0
apps/smart-pharmacy/src/main.ts

@@ -0,0 +1,31 @@
+import { initPreferences } from '@vben/preferences';
+import { unmountGlobalLoading } from '@vben/utils';
+
+import { overridesPreferences } from './preferences';
+
+/**
+ * 应用初始化完成之后再进行页面加载渲染
+ */
+async function initApplication() {
+  // name用于指定项目唯一标识
+  // 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
+  const env = import.meta.env.PROD ? 'prod' : 'dev';
+  const appVersion = import.meta.env.VITE_APP_VERSION;
+  const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
+
+  // app偏好设置初始化
+  await initPreferences({
+    namespace,
+    overrides: overridesPreferences,
+  });
+
+  // 启动应用并挂载
+  // vue应用主要逻辑及视图
+  const { bootstrap } = await import('./bootstrap');
+  await bootstrap(namespace);
+
+  // 移除并销毁loading
+  unmountGlobalLoading();
+}
+
+initApplication();

+ 42 - 0
apps/smart-pharmacy/src/preferences.ts

@@ -0,0 +1,42 @@
+import { defineOverridesPreferences } from '@vben/preferences';
+
+import dayjs from 'dayjs';
+
+/**
+ * @description 项目配置文件
+ * 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
+ * !!! 更改配置后请清空缓存,否则可能不生效
+ */
+export const overridesPreferences = defineOverridesPreferences({
+  // overrides
+  app: {
+    name: import.meta.env.VITE_APP_TITLE,
+    authPageLayout: 'panel-center',
+    layout: 'header-nav',
+    enablePreferences: false,
+  },
+  header: {
+    menuAlign: 'center',
+  },
+  logo: {
+    source: import.meta.env.VITE_APP_LOGO || '',
+  },
+  tabbar: {
+    enable: false,
+  },
+  copyright: {
+    enable: true,
+    date: dayjs().format('YYYY'),
+    companyName: '杭州六智科技有限公司',
+    companySiteLink: '',
+  },
+  theme: {
+    mode: 'light',
+  },
+  widget: {
+    globalSearch: false,
+    refresh: false,
+    notification: false,
+    themeToggle: false,
+  },
+});

+ 50 - 0
apps/smart-pharmacy/src/router/access.ts

@@ -0,0 +1,50 @@
+import type {
+  ComponentRecordType,
+  GenerateMenuAndRoutesOptions,
+} from '@vben/types';
+
+import { generateAccessible } from '@vben/access';
+import { preferences } from '@vben/preferences';
+
+import { message } from 'ant-design-vue';
+
+import { getAccessMenuMethod } from '#/api';
+import { BasicLayout, IFrameView } from '#/layouts';
+import { $t } from '#/locales';
+
+const forbiddenComponent = () => import('#/core/fallback/forbidden.vue');
+
+async function generateAccess(options: GenerateMenuAndRoutesOptions) {
+  const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
+
+  const layoutMap: ComponentRecordType = {
+    BasicLayout,
+    IFrameView,
+  };
+  return await generateAccessible(preferences.app.accessMode, {
+    ...options,
+    fetchMenuListAsync: async (permissions) => {
+      let close;
+      const id = setTimeout(() => {
+        close = message.loading({
+          content: `${$t('common.loadingMenu')}...`,
+          duration: 0,
+        });
+      }, 500);
+      close = () => clearTimeout(id);
+      const res = await fetch('/database/menu.json');
+      const data = await res.json();
+      const menus = data;
+      // const menus = await getAccessMenuMethod(permissions).catch(() => []);
+      close?.();
+      return menus as any;
+    },
+    // 可以指定没有权限跳转403页面
+    forbiddenComponent,
+    // 如果 route.meta.menuVisibleWithForbidden = true
+    layoutMap,
+    pageMap,
+  });
+}
+
+export { generateAccess };

+ 147 - 0
apps/smart-pharmacy/src/router/guard.ts

@@ -0,0 +1,147 @@
+import type { Router } from 'vue-router';
+
+import { DEFAULT_PATH, LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+import { useAccessStore, useUserStore } from '@vben/stores';
+import { startProgress, stopProgress } from '@vben/utils';
+
+import { accessRoutes, coreRouteNames } from '#/router/routes';
+import { useAuthStore } from '#/store';
+
+import { generateAccess } from './access';
+
+/**
+ * 通用守卫配置
+ * @param router
+ */
+function setupCommonGuard(router: Router) {
+  // 记录已经加载的页面
+  const loadedPaths = new Set<string>();
+
+  router.beforeEach((to) => {
+    to.meta.loaded = loadedPaths.has(to.path);
+
+    // 页面加载进度条
+    if (!to.meta.loaded && preferences.transition.progress) {
+      startProgress().then();
+    }
+    return true;
+  });
+
+  router.afterEach((to) => {
+    // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
+
+    loadedPaths.add(to.path);
+
+    // 关闭页面加载进度条
+    if (preferences.transition.progress) {
+      stopProgress().then();
+    }
+  });
+}
+
+/**
+ * 权限访问守卫配置
+ * @param router
+ */
+function setupAccessGuard(router: Router) {
+  router.beforeEach(async (to, from) => {
+    const accessStore = useAccessStore();
+    const userStore = useUserStore();
+    const authStore = useAuthStore();
+
+    // 基本路由,这些路由不需要进入权限拦截
+    if (coreRouteNames.includes(to.name as string)) {
+      if (to.path === LOGIN_PATH && accessStore.accessToken) {
+        return decodeURIComponent(
+          (to.query?.redirect as string) ||
+            userStore.userInfo?.homePath ||
+            preferences.app.defaultHomePath,
+        );
+      }
+      return true;
+    }
+
+    // accessToken 检查
+    if (!accessStore.accessToken) {
+      // 明确声明忽略权限访问权限,则可以访问
+      if (to.meta.ignoreAccess) {
+        return true;
+      }
+
+      // 没有访问权限,跳转登录页面
+      if (to.fullPath !== LOGIN_PATH) {
+        return {
+          path: LOGIN_PATH,
+          // 如不需要,直接删除 query
+          query:
+            to.fullPath === preferences.app.defaultHomePath
+              ? {}
+              : { redirect: encodeURIComponent(to.fullPath) },
+          // 携带当前跳转的页面,登录后重新跳转该页面
+          replace: true,
+        };
+      }
+      return to;
+    }
+
+    // 是否已经生成过动态路由
+    if (accessStore.isAccessChecked) {
+      return true;
+    }
+
+    // 生成路由表
+    // 当前登录用户拥有的角色标识列表
+    const userInfo = userStore.userInfo || (await authStore.getUserInfo());
+    const userRoles = userInfo.roles ?? [];
+
+    // 生成菜单和路由
+    const { accessibleMenus, accessibleRoutes } = await generateAccess({
+      roles: userRoles,
+      router,
+      // 则会在菜单中显示,但是访问会被重定向到403
+      routes: accessRoutes,
+    });
+    const path =
+      to.path === DEFAULT_PATH ? preferences.app.defaultHomePath : to.path;
+    const redirect =
+      decodeURIComponent(<string>from.query.redirect) === DEFAULT_PATH
+        ? void 0
+        : from.query.redirect;
+
+    if (!userInfo.homePath) {
+      userStore.updateHomePath(
+        preferences.app.defaultHomePath || accessibleMenus[0]?.path,
+      );
+    }
+
+    // 保存菜单信息和路由信息
+    accessStore.setAccessMenus(accessibleMenus);
+    accessStore.setAccessRoutes(accessibleRoutes);
+    accessStore.setIsAccessChecked(true);
+    const redirectPath = (redirect ??
+      (path === preferences.app.defaultHomePath
+        ? userInfo.homePath ||
+          preferences.app.defaultHomePath ||
+          accessibleMenus[0]?.path
+        : to.fullPath)) as string;
+
+    return {
+      ...router.resolve(decodeURIComponent(redirectPath)),
+      replace: true,
+    };
+  });
+}
+
+/**
+ * 项目守卫配置
+ * @param router
+ */
+function createRouterGuard(router: Router) {
+  /** 通用 */
+  setupCommonGuard(router);
+  /** 权限访问 */
+  setupAccessGuard(router);
+}
+
+export { createRouterGuard };

+ 37 - 0
apps/smart-pharmacy/src/router/index.ts

@@ -0,0 +1,37 @@
+import {
+  createRouter,
+  createWebHashHistory,
+  createWebHistory,
+} from 'vue-router';
+
+import { resetStaticRoutes } from '@vben/utils';
+
+import { createRouterGuard } from './guard';
+import { routes } from './routes';
+
+/**
+ *  @zh_CN 创建vue-router实例
+ */
+const router = createRouter({
+  history:
+    import.meta.env.VITE_ROUTER_HISTORY === 'hash'
+      ? createWebHashHistory(import.meta.env.VITE_BASE)
+      : createWebHistory(import.meta.env.VITE_BASE),
+  // 应该添加到路由的初始路由列表。
+  routes,
+  scrollBehavior: (to, _from, savedPosition) => {
+    if (savedPosition) {
+      return savedPosition;
+    }
+    return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
+  },
+  // 是否应该禁止尾部斜杠。
+  strict: true,
+});
+
+const resetRoutes = () => resetStaticRoutes(router, routes);
+
+// 创建路由守卫
+createRouterGuard(router);
+
+export { resetRoutes, router };

+ 63 - 0
apps/smart-pharmacy/src/router/routes/core.ts

@@ -0,0 +1,63 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { DEFAULT_PATH, LOGIN_PATH } from '@vben/constants';
+import { preferences } from '@vben/preferences';
+
+import { $t } from '#/locales';
+
+const BasicLayout = () => import('#/layouts/basic.vue');
+const AuthPageLayout = () => import('#/layouts/auth.vue');
+/** 全局404页面 */
+const fallbackNotFoundRoute: RouteRecordRaw = {
+  component: () => import('#/core/fallback/not-found.vue'),
+  meta: {
+    hideInBreadcrumb: true,
+    hideInMenu: true,
+    hideInTab: true,
+    title: '404',
+  },
+  name: 'FallbackNotFound',
+  path: '/:path(.*)*',
+};
+
+/** 基本路由,这些路由是必须存在的 */
+const coreRoutes: RouteRecordRaw[] = [
+  /**
+   * 根路由
+   * 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
+   * 此路由必须存在,且不应修改
+   */
+  {
+    component: BasicLayout,
+    meta: {
+      hideInBreadcrumb: true,
+      title: 'Root',
+    },
+    name: 'Root',
+    path: '/',
+    redirect: preferences.app.defaultHomePath || DEFAULT_PATH,
+    children: [],
+  },
+  {
+    component: AuthPageLayout,
+    meta: {
+      hideInTab: true,
+      title: 'Authentication',
+    },
+    name: 'Authentication',
+    path: '/auth',
+    redirect: LOGIN_PATH,
+    children: [
+      {
+        name: 'Login',
+        path: 'login',
+        component: () => import('#/core/authentication/login.vue'),
+        meta: {
+          title: $t('page.auth.login'),
+        },
+      },
+    ],
+  },
+];
+
+export { coreRoutes, fallbackNotFoundRoute };

+ 40 - 0
apps/smart-pharmacy/src/router/routes/index.ts

@@ -0,0 +1,40 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
+
+import { coreRoutes, fallbackNotFoundRoute } from './core';
+
+const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
+  eager: true,
+});
+
+// 有需要可以自行打开注释,并创建文件夹
+const externalRouteFiles = import.meta.glob('./external/**/*.ts', {
+  eager: true,
+});
+const staticRouteFiles = import.meta.glob('./static/**/*.ts', {
+  eager: true,
+});
+
+/** 动态路由 */
+const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
+
+/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
+const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
+/** 静态路由列表,访问这些页面需要权限控制 */
+const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
+
+/** 路由列表,由基本路由、外部路由和404兜底路由组成
+ *  无需走权限验证(会一直显示在菜单中) */
+const routes: RouteRecordRaw[] = [
+  ...coreRoutes,
+  ...externalRoutes,
+  fallbackNotFoundRoute,
+];
+
+/** 基本路由列表,这些路由不需要进入权限拦截 */
+const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
+
+/** 有权限校验的路由列表,包含动态路由和静态路由 */
+const accessRoutes = [...dynamicRoutes, ...staticRoutes];
+export { accessRoutes, coreRouteNames, routes };

+ 77 - 0
apps/smart-pharmacy/src/store/auth.ts

@@ -0,0 +1,77 @@
+import { computed, unref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { DEFAULT_PATH, LOGIN_PATH } from '@vben/constants';
+import { $t } from '@vben/locales';
+import { preferences } from '@vben/preferences';
+import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
+
+import { useRequest } from '@six/request';
+import { message, notification } from 'ant-design-vue';
+import { defineStore } from 'pinia';
+
+import { getUserInfoMethod, loginMethod } from '#/api/method/access';
+
+export const useAuthStore = defineStore('auth', () => {
+  const accessStore = useAccessStore();
+  const userStore = useUserStore();
+  const router = useRouter();
+
+  const login = useRequest(loginMethod, {
+    immediate: false,
+  }).onSuccess(async ({ data }) => {
+    const userInfo = await userinfo.send(data.accessToken);
+
+    await router.push(
+      userInfo.homePath || preferences.app.defaultHomePath || DEFAULT_PATH,
+    );
+    if (userInfo.realName) {
+      notification.success({
+        description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
+        duration: 3,
+        message: $t('authentication.loginSuccess'),
+      });
+    } else {
+      message.success($t('authentication.loginSuccessDesc'));
+    }
+  });
+
+  const logout = {
+    async send(redirect: boolean = true) {
+      resetAllStores();
+      accessStore.setLoginExpired(false);
+      // 回登录页带上当前路由地址
+      await router.replace({
+        path: LOGIN_PATH,
+        query: redirect
+          ? { redirect: encodeURIComponent(router.currentRoute.value.fullPath) }
+          : {},
+      });
+    },
+  };
+
+  const userinfo = useRequest(getUserInfoMethod, {
+    immediate: false,
+  }).onSuccess(({ data }) => {
+    userStore.setUserInfo(data);
+  });
+
+  function $reset() {
+    login.abort();
+    userinfo.abort();
+    accessStore.$reset();
+    userStore.$reset();
+  }
+
+  const loading = computed(() =>
+    [login, userinfo].some((method) => unref(method.loading)),
+  );
+
+  return {
+    $reset,
+    loading,
+    login: login.send,
+    logout: logout.send,
+    getUserInfo: userinfo.send,
+  };
+});

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

@@ -0,0 +1 @@
+export * from './auth';

+ 187 - 0
apps/smart-pharmacy/src/views/prescription/management/data.ts

@@ -0,0 +1,187 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { OperateModel } from '#/api/method/operate';
+
+import { $t } from '#/locales';
+
+export function useUserSearchFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'RangePicker',
+      fieldName: 'operateTimeRange',
+      label: $t('prescription.list.dateRange'),
+      componentProps: {
+        valueFormat: 'YYYY-MM-DD',
+        style: { width: '100%' },
+        placeholder: [
+          $t('prescription.list.startTime'),
+          $t('prescription.list.endTime'),
+        ],
+        inputReadOnly: true,
+      },
+    },
+    {
+      component: 'Select',
+      fieldName: 'status',
+      label: $t('prescription.list.processStatus'),
+      componentProps: {
+        options: [
+          {
+            label: '已浸泡',
+            value: 1,
+          },
+          {
+            label: '已调配',
+            value: 0,
+          },
+          {
+            label: '已打包',
+            value: 2,
+          },
+        ],
+      },
+    },
+    {
+      component: 'Input',
+      fieldName: 'itemName',
+      label: $t('prescription.list.number'),
+    },
+    {
+      component: 'Select',
+      fieldName: 'medicalInstitution',
+      label: $t('prescription.list.medicalInstitution'),
+      componentProps: {
+        options: [
+          {
+            label: '同仁堂',
+            value: 1,
+          },
+          {
+            label: '浙江省中医院',
+            value: 0,
+          },
+          {
+            label: '蒋村社区卫生服务中心',
+            value: 2,
+          },
+        ],
+      },
+    },
+    {
+      component: 'Select',
+      fieldName: 'campus',
+      label: $t('prescription.list.campus'),
+      componentProps: {
+        options: [
+          {
+            label: '萧山馆',
+            value: 1,
+          },
+          {
+            label: '湖滨院区',
+            value: 0,
+          },
+        ],
+      },
+    },
+    {
+      component: 'Select',
+      fieldName: 'decoctionCenter',
+      label: $t('prescription.list.decoctionCenter'),
+      componentProps: {
+        options: [
+          {
+            label: '重煎药中心华东区',
+            value: 1,
+          },
+          {
+            label: '杭州煎药中心',
+            value: 0,
+          },
+        ],
+      },
+    },
+    
+  ];
+}
+
+export function useUserTableColumns<T = OperateModel.Record>(
+  onActionClick?: OnActionClickFn<T>,
+): VxeTableGridOptions<T>['columns'] {
+  return [
+    { type: 'seq', title: $t('table.column.seq'), width: 50 },
+    {
+      field: 'date',
+      title: $t('prescription.list.date'),
+      minWidth: 100,
+    },
+    {
+      field: 'number',
+      title: $t('prescription.list.number'),
+      minWidth: 100,
+    },
+    {
+      field: 'medicalInstitution',
+      title: $t('prescription.list.medicalInstitution'),
+      minWidth: 100,
+    },
+    {
+      field: 'campus',
+      title: $t('prescription.list.campus'),
+      minWidth: 100,
+    },
+    {
+      field: 'patientName',
+      title: $t('prescription.list.patientName'),
+      minWidth: 100,
+    },
+    {
+      field: 'decoctionMethod',
+      title: $t('prescription.list.decoctionMethod'),
+      minWidth: 100,
+    },
+    {
+      field: 'decoctionEnterprise',
+      title: $t('prescription.list.decoctionEnterprise'),
+      minWidth: 100,
+    },
+    {
+      field: 'decoctionCenter',
+      title: $t('prescription.list.decoctionCenter'),
+      minWidth: 100,
+    },
+    {
+      field: 'deliveryMethod',
+      title: $t('prescription.list.deliveryMethod'),
+      minWidth: 100,
+    },
+    {
+      field: 'logisticsCompany',
+      title: $t('prescription.list.logisticsCompany'),
+      minWidth: 100,
+    },
+    {
+      field: 'processStatus',
+      title: $t('prescription.list.processStatus'),
+      minWidth: 100,
+    },
+    {
+      align: 'center',
+      cellRender: {
+        name: 'CellOperation',
+        options: [{ code: 'view', text: '查看' }],
+        attrs: {
+          nameField: 'name',
+          nameTitle: $t('operate.record._'),
+          onClick: onActionClick,
+        },
+      },
+      field: 'operation',
+      fixed: 'right',
+      title: $t('table.column.operation'),
+      width: 100,
+    },
+  ];
+}

+ 421 - 0
apps/smart-pharmacy/src/views/prescription/management/detail.vue

@@ -0,0 +1,421 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+import { ArrowLeft, MapPin, Phone, Building2, User, Calendar, FileText, Pill, Package, Truck, Clock, CreditCard, Stethoscope } from 'lucide-vue-next';
+import { Tabs, Descriptions, Breadcrumb } from 'ant-design-vue';
+
+const route = useRoute();
+const activeTab = ref('prescription');
+
+const prescriptionId = computed(() => route.params.id);
+
+const mockPrescription = {
+  id: prescriptionId.value || '20230923238475',
+  date: '2023-09-23',
+  status: '打包中',
+  medicalInstitution: '蒋村社区卫生服务中心',
+  campus: '-',
+  decoctionCenter: '重煎药中心华东区',
+  decoctionCode: '代煎',
+  patientName: '唐*理',
+  gender: '女',
+  age: '48岁',
+  phone: '138****1233',
+};
+
+const prescriptionDetails = {
+  chineseMedicineType: '饮片(散装)',
+  chineseMedicineForm: '汤剂',
+  prescriptionCount: 7,
+  eachPackageCount: 2,
+  totalCount: 14,
+  decoctionMark: '否',
+  decoctionTime: '200ml',
+  packageType: '袋装',
+  decoctionTimeMark: 30,
+  decoctionTimeValue: 40,
+  decoctionTimeSecond: 30,
+  decoctionTimeSecondValue: 40,
+};
+
+const deliveryInfo = {
+  deliveryMethod: '配送到家',
+  logisticsCompany: '顺丰速运',
+  recipientName: '唐*理',
+  recipientPhone: '138****2374',
+  provinceCity: '浙江省杭州市西湖区',
+  detailAddress: '蒋村街道**********',
+};
+
+const usageInfo = {
+  dailyCount: 1,
+  usageRoute: '内服',
+  usageMethod: '内服',
+  usageFrequency: '1日2次',
+  takeTime: '饭后服',
+  prescriptionMedicineCount: 18,
+  totalAmount: 234.00,
+  decoctionFee: 21.00,
+  deliveryFee: 0.00,
+  prescriptionTotalAmount: 245.00,
+  medicalInsuranceAmount: 231.90,
+  selfPayAmount: 13.10,
+  dispensingFee: 0.00,
+  prescriptionTotalAmountSecond: 245.00,
+  medicalInsuranceAmountSecond: 231.90,
+  selfPayAmountSecond: 13.10,
+};
+
+const doctorInfo = {
+  outpatientType: '住院',
+  outpatientNo: '93859496',
+  department: '中医科(20号)',
+  doctorName: '王卫国',
+  chineseMedicineName: '腰痛',
+  chineseMedicineCode: '肝肾亏虚',
+  diagnosisMethod: '补益肝肾',
+  chineseMedicineType: '腰痛',
+  chineseMedicineCodeSecond: '肝肾亏虚',
+  diagnosisMethodSecond: '补益肝肾',
+  doctorNote: '',
+};
+
+const herbalDetails = [
+  { name: '麸炒苍术', nationalCode: '06130203958365938', hospitalName: '麸炒苍术', spec: '1kg', origin: '山东', dosage: 9.0000, unit: 'g', specialMethod: 3, price: '', doctorSignature: '否' },
+];
+
+const herbalTraceDetails = [
+  { name: '黄芪', specCode: 'HQ-001', batchNo: '20230901', supplier: '亳州中药材市场', manufacturer: '亳州药业有限公司', origin: '安徽亳州', productionDate: '2023-09-01', expiryDate: '2025-09-01' },
+  { name: '当归', specCode: 'DG-002', batchNo: '20230815', supplier: '甘肃中药材市场', manufacturer: '陇西药业有限公司', origin: '甘肃陇西', productionDate: '2023-08-15', expiryDate: '2025-08-15' },
+  { name: '白芍', specCode: 'BS-003', batchNo: '20230820', supplier: '浙江中药材市场', manufacturer: '杭白芍业有限公司', origin: '浙江杭州', productionDate: '2023-08-20', expiryDate: '2025-08-20' },
+  { name: '熟地', specCode: 'SD-004', batchNo: '20230905', supplier: '河南中药材市场', manufacturer: '焦作药业有限公司', origin: '河南焦作', productionDate: '2023-09-05', expiryDate: '2025-09-05' },
+  { name: '川芎', specCode: 'CX-005', batchNo: '20230825', supplier: '四川中药材市场', manufacturer: '彭州药业有限公司', origin: '四川彭州', productionDate: '2023-08-25', expiryDate: '2025-08-25' },
+  { name: '杜仲', specCode: 'DZ-006', batchNo: '20230910', supplier: '陕西中药材市场', manufacturer: '汉中药业有限公司', origin: '陕西汉中', productionDate: '2023-09-10', expiryDate: '2025-09-10' },
+  { name: '续断', specCode: 'XD-007', batchNo: '20230818', supplier: '湖北中药材市场', manufacturer: '恩施药业有限公司', origin: '湖北恩施', productionDate: '2023-08-18', expiryDate: '2025-08-18' },
+  { name: '牛膝', specCode: 'NX-008', batchNo: '20230908', supplier: '河南中药材市场', manufacturer: '怀庆药业有限公司', origin: '河南焦作', productionDate: '2023-09-08', expiryDate: '2025-09-08' },
+];
+
+const processFlowDetails = [
+  { status: '已开始浓缩', operator: '李*霞', operationTime: '2023-09-23 10:25:38', note: '开始浓缩药液量: 2100ml', hasPhoto: true },
+  { status: '已开始煎煮', operator: '李*霞', operationTime: '2023-09-23 10:24:38', note: '加水量: 3500ml', hasPhoto: true },
+  { status: '已复核', operator: '王*霞', operationTime: '2023-09-23 10:23:38', note: '复核重量: 1949.50g', hasPhoto: true },
+  { status: '已调配', operator: '吴*', operationTime: '2023-09-23 10:22:38', note: '调配重量: 1948.31g', hasPhoto: true },
+  { status: '药房审核已通过', operator: '崔*红', operationTime: '2023-09-23 09:21:38', note: '', hasPhoto: true },
+];
+
+const logisticsInfo = {
+  company: '顺丰速运',
+  trackingNo: 'SF73648596038958987',
+  status: '已揽件',
+  statusTime: '2023-09-23 09:25:38',
+  progress: '返回内容返回内容返回内容返回内容返回内容返回内容返回内容返回内容返回内容返回内容返回内容',
+};
+
+function goBack() {
+  window.history.back();
+}
+</script>
+
+<template>
+  <Page auto-content-height>
+    <template #page-header>
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-3">
+          <button
+            class="flex items-center gap-1 text-gray-600 hover:text-gray-900 transition-colors"
+            @click="goBack"
+          >
+            <ArrowLeft class="size-5" />
+            <span>返回</span>
+          </button>
+          <Breadcrumb>
+            <Breadcrumb.Item>处方管理</Breadcrumb.Item>
+            <Breadcrumb.Item>处方列表</Breadcrumb.Item>
+            <Breadcrumb.Item>处方详情</Breadcrumb.Item>
+          </Breadcrumb>
+        </div>
+      </div>
+    </template>
+
+    <div class="bg-white rounded-lg shadow-sm p-6">
+      <div class="flex items-center justify-between mb-6 pb-4 border-b">
+        <div class="flex items-center gap-4">
+          <div class="flex items-center gap-2">
+            <FileText class="size-6 text-blue-500" />
+            <span class="text-lg font-semibold">煎药中心处方</span>
+          </div>
+          <span class="text-gray-400">|</span>
+          <span class="text-gray-600">处方号:{{ mockPrescription.id }}</span>
+        </div>
+        <div class="flex items-center gap-2">
+          <Calendar class="size-5 text-gray-400" />
+          <span class="text-gray-600">{{ mockPrescription.date }}</span>
+          <span class="text-gray-400">|</span>
+          <span class="px-3 py-1 bg-yellow-100 text-yellow-700 rounded-full text-sm">{{ mockPrescription.status }}</span>
+        </div>
+      </div>
+
+      <Descriptions :column="4" bordered :size="'small'" class="mb-6">
+        <Descriptions.Item label="医疗机构" :labelStyle="{ width: '120px' }">
+          <div class="flex items-center gap-2">
+            <Building2 class="size-4 text-gray-400" />
+            {{ mockPrescription.medicalInstitution }}
+          </div>
+        </Descriptions.Item>
+        <Descriptions.Item label="院区" :labelStyle="{ width: '120px' }">
+          {{ mockPrescription.campus }}
+        </Descriptions.Item>
+        <Descriptions.Item label="代煎中心" :labelStyle="{ width: '120px' }">
+          {{ mockPrescription.decoctionCenter }}
+        </Descriptions.Item>
+        <Descriptions.Item label="代煎代码" :labelStyle="{ width: '120px' }">
+          <span class="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-sm">{{ mockPrescription.decoctionCode }}</span>
+        </Descriptions.Item>
+        <Descriptions.Item label="患者姓名" :labelStyle="{ width: '120px' }">
+          <div class="flex items-center gap-2">
+            <User class="size-4 text-gray-400" />
+            {{ mockPrescription.patientName }}
+          </div>
+        </Descriptions.Item>
+        <Descriptions.Item label="性别" :labelStyle="{ width: '120px' }">
+          {{ mockPrescription.gender }}
+        </Descriptions.Item>
+        <Descriptions.Item label="年龄" :labelStyle="{ width: '120px' }">
+          {{ mockPrescription.age }}
+        </Descriptions.Item>
+        <Descriptions.Item label="患者电话" :labelStyle="{ width: '120px' }">
+          <div class="flex items-center gap-2">
+            <Phone class="size-4 text-gray-400" />
+            {{ mockPrescription.phone }}
+          </div>
+        </Descriptions.Item>
+      </Descriptions>
+
+      <Tabs v-model:activeKey="activeTab" type="card">
+        <Tabs.TabPane key="prescription" tab="处方详情">
+          <div class="space-y-6">
+            <div class="bg-gray-50 rounded-lg p-4">
+              <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center gap-2">
+                <Pill class="size-4 text-green-500" />
+                煎药要求
+              </h3>
+              <Descriptions :column="4" :size="'small'">
+                <Descriptions.Item label="中药类型">
+                  <span class="px-2 py-0.5 bg-green-100 text-green-700 rounded text-sm">饮片(散装)</span>
+                </Descriptions.Item>
+                <Descriptions.Item label="中药剂型">
+                  <span class="px-2 py-0.5 bg-green-100 text-green-700 rounded text-sm">汤剂</span>
+                </Descriptions.Item>
+                <Descriptions.Item label="处方剂数">{{ prescriptionDetails.prescriptionCount }} 剂</Descriptions.Item>
+                <Descriptions.Item label="每剂包数">{{ prescriptionDetails.eachPackageCount }} 包</Descriptions.Item>
+                <Descriptions.Item label="总包数">{{ prescriptionDetails.totalCount }} 包</Descriptions.Item>
+                <Descriptions.Item label="煎药标记">
+                  <span class="px-2 py-0.5 bg-red-100 text-red-700 rounded text-sm">{{ prescriptionDetails.decoctionMark }}</span>
+                </Descriptions.Item>
+                <Descriptions.Item label="煎药时间">{{ prescriptionDetails.decoctionTime }}</Descriptions.Item>
+                <Descriptions.Item label="包装类型">
+                  <span class="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-sm">{{ prescriptionDetails.packageType }}</span>
+                </Descriptions.Item>
+                <Descriptions.Item label="浸泡时间">{{ prescriptionDetails.decoctionTimeMark }}分钟</Descriptions.Item>
+                <Descriptions.Item label="煎煮时间">{{ prescriptionDetails.decoctionTimeValue }}分钟</Descriptions.Item>
+                <Descriptions.Item label="二煎浸泡时间">{{ prescriptionDetails.decoctionTimeSecond }}分钟</Descriptions.Item>
+                <Descriptions.Item label="二煎煎煮时间">{{ prescriptionDetails.decoctionTimeSecondValue }}分钟</Descriptions.Item>
+              </Descriptions>
+            </div>
+
+            <div class="bg-gray-50 rounded-lg p-4">
+              <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center gap-2">
+                <Truck class="size-4 text-orange-500" />
+                配送信息
+              </h3>
+              <Descriptions :column="4" :size="'small'">
+                <Descriptions.Item label="配送方式">{{ deliveryInfo.deliveryMethod }}</Descriptions.Item>
+                <Descriptions.Item label="物流公司">{{ deliveryInfo.logisticsCompany }}</Descriptions.Item>
+                <Descriptions.Item label="收件人">{{ deliveryInfo.recipientName }}</Descriptions.Item>
+                <Descriptions.Item label="收件电话">{{ deliveryInfo.recipientPhone }}</Descriptions.Item>
+                <Descriptions.Item label="收件省市" :span="2">
+                  <div class="flex items-center gap-2">
+                    <MapPin class="size-4 text-gray-400" />
+                    {{ deliveryInfo.provinceCity }}
+                  </div>
+                </Descriptions.Item>
+                <Descriptions.Item label="详细地址" :span="2">
+                  <div class="flex items-center gap-2">
+                    <MapPin class="size-4 text-gray-400" />
+                    {{ deliveryInfo.detailAddress }}
+                  </div>
+                </Descriptions.Item>
+              </Descriptions>
+            </div>
+
+            <div class="bg-gray-50 rounded-lg p-4">
+              <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center gap-2">
+                <Clock class="size-4 text-blue-500" />
+                用药信息和金额
+              </h3>
+              <Descriptions :column="4" :size="'small'">
+                <Descriptions.Item label="每日剂数">{{ usageInfo.dailyCount }} 剂</Descriptions.Item>
+                <Descriptions.Item label="用药途径">{{ usageInfo.usageRoute }}</Descriptions.Item>
+                <Descriptions.Item label="用药方法">{{ usageInfo.usageMethod }}</Descriptions.Item>
+                <Descriptions.Item label="使用频次">{{ usageInfo.usageFrequency }}</Descriptions.Item>
+                <Descriptions.Item label="服药时间">{{ usageInfo.takeTime }}</Descriptions.Item>
+                <Descriptions.Item label="处方药味数">{{ usageInfo.prescriptionMedicineCount }} 味</Descriptions.Item>
+                <Descriptions.Item label="药品总金额">¥{{ usageInfo.totalAmount.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="代煎或加工费金额">¥{{ usageInfo.decoctionFee.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="配送金额">¥{{ usageInfo.deliveryFee.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="处方总金额">¥{{ usageInfo.prescriptionTotalAmount.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="医保报销金额">¥{{ usageInfo.medicalInsuranceAmount.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="自付金额">¥{{ usageInfo.selfPayAmount.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="调配金额">¥{{ usageInfo.dispensingFee.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="处方总金额">¥{{ usageInfo.prescriptionTotalAmountSecond.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="医保报销金额">¥{{ usageInfo.medicalInsuranceAmountSecond.toFixed(2) }}</Descriptions.Item>
+                <Descriptions.Item label="自付金额">¥{{ usageInfo.selfPayAmountSecond.toFixed(2) }}</Descriptions.Item>
+              </Descriptions>
+            </div>
+
+            <div class="bg-gray-50 rounded-lg p-4">
+              <h3 class="text-sm font-medium text-gray-700 mb-4 flex items-center gap-2">
+                <CreditCard class="size-4 text-purple-500" />
+                就诊信息
+              </h3>
+              <Descriptions :column="4" :size="'small'">
+                <Descriptions.Item label="门诊/住院">{{ doctorInfo.outpatientType }}</Descriptions.Item>
+                <Descriptions.Item label="门诊/住院号">{{ doctorInfo.outpatientNo }}</Descriptions.Item>
+                <Descriptions.Item label="科室/病区">{{ doctorInfo.department }}</Descriptions.Item>
+                <Descriptions.Item label="医生姓名">
+                  <div class="flex items-center gap-2">
+                    <Stethoscope class="size-4 text-gray-400" />
+                    {{ doctorInfo.doctorName }}
+                  </div>
+                </Descriptions.Item>
+                <Descriptions.Item label="中医病名">{{ doctorInfo.chineseMedicineName }}</Descriptions.Item>
+                <Descriptions.Item label="中医证型">{{ doctorInfo.chineseMedicineCode }}</Descriptions.Item>
+                <Descriptions.Item label="治则治法">{{ doctorInfo.diagnosisMethod }}</Descriptions.Item>
+                <Descriptions.Item label="中医病名">{{ doctorInfo.chineseMedicineType }}</Descriptions.Item>
+                <Descriptions.Item label="中医证型">{{ doctorInfo.chineseMedicineCodeSecond }}</Descriptions.Item>
+                <Descriptions.Item label="治则治法">{{ doctorInfo.diagnosisMethodSecond }}</Descriptions.Item>
+                <Descriptions.Item label="医生备注" :span="2">{{ doctorInfo.doctorNote || '-' }}</Descriptions.Item>
+              </Descriptions>
+            </div>
+          </div>
+        </Tabs.TabPane>
+
+        <Tabs.TabPane key="herbal" tab="饮片明细">
+          <div class="bg-gray-50 rounded-lg p-4">
+            <div class="overflow-x-auto">
+              <table class="w-full text-sm">
+                <thead>
+                  <tr class="bg-white">
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">药品名称</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">国标编码</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">医疗机构药品名称</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">规格</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">产地</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">剂量</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">药品单位</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">中药特殊煎法</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">药品单价(元)</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">医生签名</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="(item, index) in herbalDetails" :key="index" class="bg-white hover:bg-gray-50">
+                    <td class="px-4 py-3 border-b">{{ item.name }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.nationalCode }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.hospitalName }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.spec }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.origin }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.dosage }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.unit }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.specialMethod }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.price || '-' }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.doctorSignature }}</td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </Tabs.TabPane>
+
+        <Tabs.TabPane key="flow" tab="流程物流追溯">
+          <div class="bg-gray-50 rounded-lg p-4">
+            <div class="grid grid-cols-2 gap-4">
+              <div class="bg-white rounded-lg p-4">
+                <h3 class="text-sm font-medium text-gray-700 mb-4">处方流转流程</h3>
+                <div class="relative pl-6 border-l-2 border-gray-200">
+                  <div v-for="(item, index) in processFlowDetails" :key="index" class="relative pb-4 last:pb-0">
+                    <div class="absolute -left-[9px] w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
+                    <div class="ml-2">
+                      <div class="text-sm font-medium text-gray-800">{{ item.status }}</div>
+                      <div class="text-xs text-gray-500 mt-1">操作人: {{ item.operator }}</div>
+                      <div class="text-xs text-gray-500">操作时间: {{ item.operationTime }}</div>
+                      <div v-if="item.note" class="text-xs text-gray-500">备注: {{ item.note }}</div>
+                      <div v-if="item.hasPhoto" class="mt-1">
+                        <a href="#" class="text-xs text-blue-500 hover:text-blue-700">环节照片/视频: 查看照片</a>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+
+              <div class="bg-white rounded-lg p-4">
+                <h3 class="text-sm font-medium text-gray-700 mb-4">物流跟踪</h3>
+                <div class="flex items-center justify-between mb-4">
+                  <span class="text-sm text-gray-500">{{ logisticsInfo.company }} {{ logisticsInfo.trackingNo }}</span>
+                  <span class="px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-xs">{{ logisticsInfo.status }}</span>
+                </div>
+                <div class="relative pl-6 border-l-2 border-gray-200">
+                  <div class="absolute -left-[9px] w-4 h-4 bg-yellow-500 rounded-full border-2 border-white"></div>
+                  <div class="ml-2">
+                    <div class="text-sm font-medium text-gray-800">{{ logisticsInfo.status }}</div>
+                    <div class="text-xs text-gray-500 mt-1">{{ logisticsInfo.statusTime }}</div>
+                    <div class="text-xs text-gray-500 mt-1">{{ logisticsInfo.progress }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </Tabs.TabPane>
+
+        <Tabs.TabPane key="trace" tab="饮片追溯">
+          <div class="bg-gray-50 rounded-lg p-4">
+            <div class="overflow-x-auto">
+              <table class="w-full text-sm">
+                <thead>
+                  <tr class="bg-white">
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">饮片名称</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">规格编码</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">饮片批号</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">供应商</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">生产厂家</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">饮片产地</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">生产日期</th>
+                    <th class="px-4 py-3 text-left font-medium text-gray-700 border-b">有效期至</th>
+                    <th class="px-4 py-3 text-center font-medium text-gray-700 border-b">检验报告</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="(item, index) in herbalTraceDetails" :key="index" class="bg-white hover:bg-gray-50">
+                    <td class="px-4 py-3 border-b">{{ item.name }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.specCode }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.batchNo }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.supplier }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.manufacturer }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.origin }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.productionDate }}</td>
+                    <td class="px-4 py-3 border-b">{{ item.expiryDate }}</td>
+                    <td class="px-4 py-3 border-b text-center">
+                      <a href="#" class="text-blue-500 hover:text-blue-700 underline">查看</a>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </Tabs.TabPane>
+      </Tabs>
+    </div>
+  </Page>
+</template>

+ 173 - 0
apps/smart-pharmacy/src/views/prescription/management/list.vue

@@ -0,0 +1,173 @@
+<script lang="ts" setup>
+import type {
+  OnActionClickParams,
+  VxeTableGridOptions,
+} from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api';
+
+import { useRouter } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+
+import { useUserSearchFormSchema, useUserTableColumns } from './data';
+
+const mockData = {
+  TotalRecordCount: 5,
+  TotalPageCount: 1,
+  PageSize: 20,
+  ButtonRight: '',
+  CurrentPageSize: 14,
+  Items: [
+    {
+      id: '111',
+      date: '2026-03-09',
+      number: '2023092323945',
+      medicalInstitution: '蒋村社区卫生服务中心',
+      campus: '萧山馆',
+      patientName: '张*行',
+      decoctionMethod: '代煎',
+      decoctionEnterprise: '重煎药中心',
+      decoctionCenter: '重煎药中心华东区',
+      deliveryMethod: '配送到医院',
+      logisticsCompany: '顺丰速运',
+      processStatus: '已打包',
+    },
+    {
+      id: '222',
+      date: '2026-03-11',
+      number: '2023092312934',
+      medicalInstitution: '浙江省中医院',
+      campus: '萧山馆',
+      patientName: '赵*方',
+      decoctionMethod: '代煎',
+      decoctionEnterprise: '重煎药中心',
+      decoctionCenter: '重煎药中心华东区',
+      deliveryMethod: '配送到医院',
+      logisticsCompany: '邮政快递',
+      processStatus: '已调配',
+    },
+    {
+      id: '333',
+      date: '2026-03-12',
+      number: '2023092329384',
+      medicalInstitution: '同仁堂',
+      campus: '湖滨院区',
+      patientName: '李*当',
+      decoctionMethod: '代煎',
+      decoctionEnterprise: '杭州药中心',
+      decoctionCenter: '杭州煎药中心',
+      deliveryMethod: '配送到家',
+      logisticsCompany: '邮政快递',
+      processStatus: '已打包',
+    },
+    {
+      id: '444',
+      date: '2026-03-13',
+      number: '2023092329384',
+      medicalInstitution: '同仁堂',
+      campus: '',
+      patientName: '王*冰',
+      decoctionMethod: '自煎/代配',
+      decoctionEnterprise: '',
+      decoctionCenter: '',
+      deliveryMethod: '不配送',
+      logisticsCompany: '',
+      processStatus: '已调配',
+    },
+    {
+      id: '555',
+      date: '2026-03-13',
+      number: '2023092392834',
+      medicalInstitution: '浙江省中医院',
+      campus: '湖滨院区',
+      patientName: '刘*鑫',
+      decoctionMethod: '代煎',
+      decoctionEnterprise: '杭州药中心',
+      decoctionCenter: '杭州煎药中心',
+      deliveryMethod: '配送到家',
+      logisticsCompany: '顺丰速运',
+      processStatus: '已浸泡',
+    },
+  ],
+  PageIndex: 1,
+};
+const router = useRouter();
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useUserSearchFormSchema(),
+    submitOnChange: true,
+  },
+  gridOptions: {
+    columns: useUserTableColumns(onActionClick),
+    height: 'auto',
+    keepSource: true,
+    // proxyConfig: {
+    //   ajax: {
+    //     query({ page }, formValues) {
+    //       // 处理时间范围字段分离
+    //       const processedValues = { ...formValues };
+    //       if (
+    //         processedValues.operateTimeRange &&
+    //         Array.isArray(processedValues.operateTimeRange)
+    //       ) {
+    //         const [startTime, endTime] = processedValues.operateTimeRange;
+    //         processedValues.startTime = startTime;
+    //         processedValues.endTime = endTime;
+    //         delete processedValues.operateTimeRange; // 删除原始字段
+    //       }
+    //       return listRecordsMethod(
+    //         page.currentPage,
+    //         page.pageSize,
+    //         processedValues,
+    //       );
+    //     },
+    //   },
+    // },
+    proxyConfig: {
+      response: {
+        result: 'Data.Items',
+        total: 'Data.TotalRecordCount',
+      },
+
+      ajax: {
+        query() {
+          return Promise.resolve({
+            Data: mockData,
+            ResultInfo: '操作成功',
+            ResultCode: 0,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+    },
+  } as VxeTableGridOptions<SystemModel.User>,
+});
+function onActionClick(e: OnActionClickParams<SystemModel.User>) {
+  console.log('onActionClick---查看---', e);
+  switch (e.code) {
+    case 'view': {
+      onViewHandle(e.row);
+      break;
+    }
+  }
+}
+// 刷新
+function onRefresh() {
+  gridApi.query();
+}
+
+function onViewHandle(row: SystemModel.User) {
+  router.push(`/prescription/detail/${row.id}`);
+}
+</script>
+<template>
+  <Page auto-content-height>
+    <FormModal @success="onRefresh" />
+    <Grid />
+  </Page>
+</template>

+ 94 - 0
apps/smart-pharmacy/src/views/prescription/management/modules/form.vue

@@ -0,0 +1,94 @@
+<script lang="ts" setup>
+import type { OperateModel } from '#/api/method/operate';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Divider, Image } from 'ant-design-vue';
+
+import { getRecordDetailMethod } from '#/api/method/operate';
+import { $t } from '#/locales';
+
+const detail = ref<OperateModel.Record>({} as OperateModel.Record);
+
+const title = computed(() => $t('operate.record.title'));
+
+const [Modal, modalApi] = useVbenModal({
+  showConfirmButton: false,
+  async onOpenChange(isOpen) {
+    if (isOpen) {
+      const data = modalApi.getData<OperateModel.Record>();
+      if (data && data.id) {
+        const res = await getRecordDetailMethod(data.id);
+        console.log('getRecordDetailMethod res', res);
+        detail.value = res || ({} as OperateModel.Record);
+      }
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="title">
+    <div class="select-text p-4">
+      <div class="space-y-4 rounded-b">
+        <div class="font-bold" v-if="detail.itemName">
+          {{ $t('operate.record.name') }}:
+          {{ detail.itemName || '-' }}
+        </div>
+        <div class="font-bold" v-if="detail.operateDate">
+          {{ $t('operate.record.operationTime') }}:
+          {{ detail.operateDate || '-' }}
+        </div>
+        <div v-if="detail.operateUserName">
+          {{ $t('operate.record.operationUser') }}:
+          {{ detail.operateUserName || '-' }}
+        </div>
+        <div v-if="detail.treatmentTime">
+          {{ $t('operate.record.treatmentTime') }}:
+          {{ detail.treatmentTime || '-' }}{{ $t('operate.record.minutes') }}
+        </div>
+        <div v-if="detail.remark">
+          {{ $t('operate.record.treatmentDescription') }}:
+          {{ detail.remark || '-' }}
+        </div>
+        <div class="flex items-start space-x-2" v-if="detail.treatmentImageUrl">
+          <span class="leading-8">
+            {{ $t('operate.record.treatmentPhoto') }}:
+          </span>
+          <Image
+            v-if="detail.treatmentImageUrl"
+            :src="detail.treatmentImageUrl"
+            alt=""
+            class="border object-cover"
+            style="width: 180px; height: 90px"
+          />
+          <span v-else class="leading-8">-</span>
+        </div>
+        <Divider />
+        <div v-if="detail.name">
+          {{ $t('operate.record.patientName') }}:
+          {{ detail.name || '-' }}
+        </div>
+        <div class="flex">
+          <div v-if="detail.issueDoctorName" class="mr-10">
+            {{ $t('operate.record.openDoctor') }}:
+            {{ detail.issueDoctorName || '-' }}
+          </div>
+          <div v-if="detail.issueDate">
+            {{ $t('operate.record.openTime') }}:{{ detail.issueDate || '-' }}
+          </div>
+        </div>
+        <div v-if="detail.acupointName">
+          {{ $t('operate.record.acupoint') }}:
+          {{ detail.acupointName?.join(',') || '-' }}
+        </div>
+        <div v-if="detail.describe">
+          {{ $t('operate.record.description') }}:
+          {{ detail.describe || '-' }}
+        </div>
+      </div>
+    </div>
+  </Modal>
+</template>

+ 98 - 0
apps/smart-pharmacy/src/views/system/enterprise/data.ts

@@ -0,0 +1,98 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api/method/system';
+
+import { listOrganizationsMethodAll } from '#/api/method/system';
+import { $t } from '#/locales';
+
+export function useUserSearchFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.enterprise.name'),
+    },
+    {
+      component: 'Select',
+      fieldName: 'status',
+      label: $t('system.enterprise.type'),
+      componentProps: {
+        options: [
+          {
+            label: '煎药企业',
+            value: 1,
+          },
+          {
+            label: '饮片供应商',
+            value: 0,
+          },
+          {
+            label: '物流公司',
+            value: 2,
+          },
+        ],
+      },
+    },
+  ];
+}
+
+export function useUserTableColumns<T = SystemModel.Organization>(
+  onActionClick?: OnActionClickFn<T>,
+): VxeTableGridOptions<T>['columns'] {
+  return [
+    { type: 'seq', title: $t('table.column.seq'), width: 50 },
+    {
+      field: 'name',
+      title: $t('system.enterprise.name'),
+      minWidth: 100,
+    },
+    {
+      field: 'code',
+      title: $t('system.enterprise.code'),
+      minWidth: 100,
+    },
+    {
+      field: 'type',
+      title: $t('system.enterprise.type'),
+      minWidth: 100,
+    },
+    {
+      field: 'remark',
+      title: $t('system.enterprise.remark'),
+      minWidth: 100,
+    },
+    {
+      field: 'createTime',
+      title: $t('system.organization.createTime'),
+      minWidth: 100,
+    },
+    {
+      field: 'createUser',
+      title: $t('system.organization.createUser'),
+      minWidth: 100,
+    },
+    {
+      align: 'center',
+      // cellRender: {
+      //   attrs: {
+      //     nameField: 'name',
+      //     nameTitle: $t('system.user._'),
+      //     onClick: onActionClick,
+      //   },
+      //   name: 'CellOperation',
+      // },
+      field: 'operation',
+      fixed: 'right',
+      title: $t('table.column.operation'),
+      width: 130,
+    },
+  ];
+}
+
+export function useUserFormSchema(
+  current?: Pick<SystemModel.Organization, 'id' | 'name'>,
+): VbenFormSchema[] {
+  return [];
+}

+ 108 - 0
apps/smart-pharmacy/src/views/system/enterprise/list.vue

@@ -0,0 +1,108 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+
+import { useUserSearchFormSchema, useUserTableColumns } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useUserSearchFormSchema(),
+    submitOnChange: true,
+  },
+  gridOptions: {
+    columns: useUserTableColumns(),
+    height: 'auto',
+    keepSource: true,
+    // proxyConfig: {
+    //   ajax: {
+    //     query({ page }, formValues) {
+    //       return listOrganizationsMethod(
+    //         page.currentPage,
+    //         page.pageSize,
+    //         formValues,
+    //       );
+    //     },
+    //   },
+    // },
+    // rowConfig: {
+    //   keyField: 'id',
+    // },
+
+    proxyConfig: {
+      ajax: {
+        query() {
+          return Promise.resolve({
+            Data: {
+              TotalRecordCount: 3,
+
+              Items: [
+                {
+                  pid: '1',
+                  name: '重药控股有限公司',
+                  code: 'cyeast',
+                  createUser: 'createUser',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '煎药企业',
+                  remark: '备注1',
+                },
+                {
+                  pid: '2',
+                  name: '浙江药业公司',
+                  code: 'zjeast',
+                  createUser: 'createUser',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '饮片供应商',
+                  remark: '备注2',
+                },
+                {
+                  pid: '3',
+                  name: '中通快递',
+                  code: 'ztkuaidi',
+                  createUser: 'createUser',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '物流公司',
+                  remark: '备注333',
+                },
+              ],
+            },
+
+            ResultCode: 0,
+          });
+        },
+      },
+
+      response: {
+        result: 'Data.Items',
+        total: 'Data.TotalRecordCount',
+      },
+    },
+
+    rowConfig: {
+      keyField: 'pid',
+    },
+  } as VxeTableGridOptions<SystemModel.User>,
+});
+
+// 刷新
+function onRefresh() {
+  gridApi.query();
+}
+</script>
+<template>
+  <Page auto-content-height>
+    <FormModal @success="onRefresh" />
+    <Grid>
+      <template #toolbar-tools></template>
+    </Grid>
+  </Page>
+</template>

+ 49 - 0
apps/smart-pharmacy/src/views/system/enterprise/modules/form.vue

@@ -0,0 +1,49 @@
+<script lang="ts" setup>
+import type { SystemModel } from '#/api';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { $t } from '#/locales';
+
+import { useUserFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const formData = ref<SystemModel.Organization>();
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', [$t('system.organization._')])
+    : $t('ui.actionTitle.create', [$t('system.organization._')]);
+});
+
+const [Form, formApi] = useVbenForm({
+  // layout: 'vertical',
+  schema: useUserFormSchema(),
+  showDefaultActions: false,
+});
+
+function resetForm() {
+  formApi.resetForm();
+  formApi.setValues(formData.value || {});
+}
+
+const [Modal, modalApi] = useVbenModal({});
+</script>
+
+<template>
+  <Modal :title="getTitle">
+    <Form class="mx-4" />
+    <template #prepend-footer>
+      <div class="flex-auto">
+        <Button type="primary" danger @click="resetForm">
+          {{ $t('common.reset') }}
+        </Button>
+      </div>
+    </template>
+  </Modal>
+</template>

+ 213 - 0
apps/smart-pharmacy/src/views/system/organization/data.ts

@@ -0,0 +1,213 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api/method/system';
+
+import { listOrganizationsMethodAll } from '#/api/method/system';
+import { $t } from '#/locales';
+
+export function useUserSearchFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.organization.name'),
+    },
+    {
+      component: 'Select',
+      fieldName: 'status',
+      label: $t('system.organization.type'),
+      componentProps: {
+        options: [
+          {
+            label: '院区',
+            value: 1,
+          },
+          {
+            label: '管理部门',
+            value: 0,
+          },
+        ],
+      },
+    },
+  ];
+}
+
+export function useUserTableColumns<T = SystemModel.Organization>(
+  onActionClick?: OnActionClickFn<T>,
+): VxeTableGridOptions<T>['columns'] {
+  return [
+    { type: 'seq', title: $t('table.column.seq'), width: 50 },
+    {
+      field: 'name',
+      title: $t('system.organization.name'),
+      minWidth: 100,
+    },
+    {
+      field: 'code',
+      title: $t('system.organization.code'),
+      minWidth: 100,
+    },
+    {
+      field: 'type',
+      title: $t('system.organization.type'),
+      minWidth: 100,
+    },
+    {
+      field: 'parentinstitutionSelsourceName',
+      title: $t('system.organization.superior'),
+      minWidth: 100,
+    },
+    {
+      field: 'creditCode',
+      title: $t('system.organization.creditCode'),
+      minWidth: 100,
+    },
+    {
+      field: 'medicineCenter',
+      title: $t('system.organization.medicineCenter'),
+      minWidth: 100,
+    },
+    {
+      field: 'remark',
+      title: $t('system.organization.remark'),
+      minWidth: 100,
+    },
+    {
+      field: 'createTime',
+      title: $t('system.organization.createTime'),
+      minWidth: 100,
+    },
+    {
+      field: 'createUser',
+      title: $t('system.organization.createUser'),
+      minWidth: 100,
+    },
+    {
+      align: 'center',
+      // cellRender: {
+      //   attrs: {
+      //     nameField: 'name',
+      //     nameTitle: $t('system.user._'),
+      //     onClick: onActionClick,
+      //   },
+      //   name: 'CellOperation',
+      // },
+      field: 'operation',
+      fixed: 'right',
+      title: $t('table.column.operation'),
+      width: 130,
+    },
+  ];
+}
+
+export function useUserFormSchema(
+  current?: Pick<SystemModel.Organization, 'id' | 'name'>,
+): VbenFormSchema[] {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.organization.name'),
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: $t('system.organization.input'),
+      },
+      fieldName: 'code',
+      label: $t('system.organization.code'),
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: $t('system.organization.type'),
+      },
+      fieldName: 'type',
+      label: $t('system.organization.type'),
+      rules: 'required',
+    },
+    // {
+    //   component: 'ApiSelect',
+    //   componentProps: {
+    //     allowClear: true,
+    //     api: listOrganizationsMethodAll,
+    //     class: 'w-full',
+    //     labelField: 'name',
+    //     valueField: 'pid',
+    //     childrenField: 'children',
+    //     afterFetch: (res: SystemModel.Organization[]) => {
+    //       if (!current) return res;
+    //       return Array.isArray(res)
+    //         ? res.filter(
+    //             (item) => item.pid !== current.id && item.name !== current.name,
+    //           )
+    //         : res;
+    //     },
+    //   },
+    //   fieldName: 'parentInstitutionId',
+    //   label: $t('system.organization.superior'),
+    // },
+    {
+      component: 'Select',
+
+      componentProps: {
+        allowClear: true,
+
+        class: 'w-full',
+
+        options: [
+          {
+            label: '测试1上级机构',
+            value: '1',
+          },
+
+          {
+            label: '测试2上级机构',
+            value: '2',
+          },
+
+          {
+            label: '测试3上级机构',
+            value: '3',
+          },
+        ],
+      },
+
+      fieldName: 'parentInstitutionId',
+
+      label: $t('system.organization.superior'),
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: $t('system.organization.creditCode'),
+      },
+      fieldName: 'creditCode',
+      labelWidth: 110,
+      label: $t('system.organization.creditCode'),
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: $t('system.organization.medicineCenter'),
+      },
+      fieldName: 'medicineCenter',
+      label: $t('system.organization.medicineCenter'),
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      componentProps: {
+        placeholder: $t('system.organization.remark'),
+      },
+      fieldName: 'remark',
+      label: $t('system.organization.remark'),
+      rules: 'required',
+    },
+  ];
+}

+ 164 - 0
apps/smart-pharmacy/src/views/system/organization/list.vue

@@ -0,0 +1,164 @@
+<script lang="ts" setup>
+import type {
+  OnActionClickParams,
+  VxeTableGridOptions,
+} from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+
+import { useUserSearchFormSchema, useUserTableColumns } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useUserSearchFormSchema(),
+    submitOnChange: true,
+  },
+  gridOptions: {
+    columns: useUserTableColumns(onActionClick),
+    height: 'auto',
+    keepSource: true,
+    // proxyConfig: {
+    //   ajax: {
+    //     query({ page }, formValues) {
+    //       return listOrganizationsMethod(
+    //         page.currentPage,
+    //         page.pageSize,
+    //         formValues,
+    //       );
+    //     },
+    //   },
+    // },
+    // rowConfig: {
+    //   keyField: 'id',
+    // },
+
+    proxyConfig: {
+      ajax: {
+        query() {
+          return Promise.resolve({
+            Data: {
+              TotalRecordCount: 3,
+
+              Items: [
+                {
+                  pid: '1',
+                  name: '六只五常院区',
+                  code: 'lz0003',
+                  parentinstitutionSelsourceName: '杭州六只医院',
+                  createUser: '张三',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '院区',
+                  creditCode: '123456789',
+                  medicineCenter: '煎药中心1',
+                  remark: '备注1',
+                },
+                {
+                  pid: '2',
+                  name: '杭州卫健委',
+                  code: 'hz9876',
+                  parentinstitutionSelsourceName: '',
+                  createUser: '张三',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '管理部门',
+                  creditCode: '987654321',
+                  medicineCenter: '煎药中心2',
+                  remark: '备注2',
+                },
+                {
+                  pid: '3',
+                  name: '同仁堂',
+                  code: 'trt0001',
+                  parentinstitutionSelsourceName: '杭州卫健委',
+                  createUser: '张三',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '医疗机构',
+                  creditCode: '555555555',
+                  medicineCenter: '煎药中心3',
+                  remark: '无',
+                },
+              ],
+            },
+
+            ResultCode: 0,
+          });
+        },
+      },
+
+      response: {
+        result: 'Data.Items',
+        total: 'Data.TotalRecordCount',
+      },
+    },
+
+    rowConfig: {
+      keyField: 'pid',
+    },
+  } as VxeTableGridOptions<SystemModel.User>,
+});
+// 表格的操作 删除和修改功能
+function onActionClick(e: OnActionClickParams<SystemModel.User>) {
+  switch (e.code) {
+    case 'delete': {
+      onDeleteHandle(e.row);
+      break;
+    }
+    case 'edit': {
+      onEditHandle(e.row);
+      break;
+    }
+  }
+}
+// 刷新
+function onRefresh() {
+  gridApi.query();
+}
+
+// 修改
+function onEditHandle(row?: SystemModel.User) {
+  // console.log('row', row);
+  formModalApi.setData(row ?? {}).open();
+}
+
+// 删除
+async function onDeleteHandle(row: SystemModel.Organization) {
+  console.log('delete', row);
+  // const hideLoading = message.loading({
+  //   content: $t('ui.actionMessage.deleting', [row.name]),
+  //   duration: 0,
+  //   key: 'action_process_msg',
+  // });
+  // try {
+  //   await deleteOrganizationMethod(row);
+  //   message.success({
+  //     content: $t('ui.actionMessage.deleteSuccess', [row.name]),
+  //     key: 'action_process_msg',
+  //   });
+  //   // 删除之后重新刷新页面
+  //   onRefresh();
+  // } finally {
+  //   hideLoading();
+  // }
+}
+</script>
+<template>
+  <Page auto-content-height>
+    <FormModal @success="onRefresh" />
+    <Grid>
+      <template #toolbar-tools>
+        <!-- <Button type="primary" @click="onEditHandle()">
+          <Plus class="size-5" />
+          {{ $t('ui.actionTitle.create', [$t('system.organization._')]) }}
+        </Button> -->
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 90 - 0
apps/smart-pharmacy/src/views/system/organization/modules/form.vue

@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import type { SystemModel } from '#/api';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { $t } from '#/locales';
+
+import { useUserFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+// const edit = useRequest(editOrganizationMethod, { immediate: false }).onSuccess(
+//   () => {
+//     emit('success');
+//   },
+// );
+const edit = {
+  send() {
+    console.log('修改,确定');
+    return Promise.resolve();
+  },
+};
+
+const formData = ref<SystemModel.Organization>();
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', [$t('system.organization._')])
+    : $t('ui.actionTitle.create', [$t('system.organization._')]);
+});
+
+const [Form, formApi] = useVbenForm({
+  // layout: 'vertical',
+  schema: useUserFormSchema(),
+  showDefaultActions: false,
+});
+
+function resetForm() {
+  formApi.resetForm();
+  formApi.setValues(formData.value || {});
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (valid) {
+      modalApi.lock();
+      const data = await formApi.getValues();
+      try {
+        await edit.send({ ...formData.value, ...data });
+        await modalApi.close();
+      } finally {
+        modalApi.lock(false);
+      }
+    }
+  },
+  onOpenChange(isOpen) {
+    if (isOpen) {
+      const data = modalApi.getData<SystemModel.Organization>();
+      if (data) {
+        if (data.id) {
+          // 编辑态:重建 schema,传入当前机构信息(id 与 name)以便过滤自身
+          formApi.setState(() => ({
+            schema: useUserFormSchema({ id: data.id, name: data.name as any }),
+          }));
+        }
+        formData.value = data;
+        formApi.setValues(formData.value);
+      }
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle">
+    <Form class="mx-4" />
+    <template #prepend-footer>
+      <div class="flex-auto">
+        <Button type="primary" danger @click="resetForm">
+          {{ $t('common.reset') }}
+        </Button>
+      </div>
+    </template>
+  </Modal>
+</template>

+ 128 - 0
apps/smart-pharmacy/src/views/system/role/data.ts

@@ -0,0 +1,128 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api';
+
+import { z } from '#/adapter/form';
+import { $t } from '#/locales';
+
+export function useRoleSearchFormSchema(): VbenFormSchema[] {
+  return [
+    { component: 'Input', fieldName: 'name', label: $t('system.role.name') },
+    {
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: $t('common.enabled'), value: 1 },
+          { label: $t('common.disabled'), value: 0 },
+        ],
+      },
+      fieldName: 'status',
+      label: $t('system.role.status'),
+    },
+  ];
+}
+
+export function useRoleTableColumns<T = SystemModel.Role>(
+  onActionClick: OnActionClickFn<T>,
+  onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
+): VxeTableGridOptions['columns'] {
+  return [
+    { type: 'seq', title: $t('table.column.seq'), width: 50 },
+    {
+      field: 'name',
+      title: $t('system.role.name'),
+      width: 200,
+    },
+    {
+      field: 'roleCode',
+      title: $t('table.column.roleCode'),
+    },
+    {
+      field: 'remark',
+      title: $t('table.column.remark'),
+    },
+    {
+      field: 'lastTime',
+      title: $t('table.column.lastTime'),
+    },
+    {
+      field: 'createUser',
+      title: $t('table.column.createUser'),
+      width: 130,
+    },
+    {
+      cellRender: {
+        attrs: { beforeChange: onStatusChange },
+        // props: { accessRole: '超级管理员' },
+        name: onStatusChange ? 'CellSwitch' : 'CellTag',
+      },
+      field: 'status',
+      title: $t('system.role.enabledStatus'),
+      width: 200,
+    },
+    {
+      align: 'center',
+      cellRender: {
+        options: ['edit', 'set', 'delete'],
+        attrs: {
+          nameField: 'name',
+          nameTitle: $t('system.role.name'),
+          onClick: onActionClick,
+        },
+        name: 'CellOperation',
+      },
+      field: 'operation',
+      fixed: 'right',
+      title: $t('table.column.operation'),
+      width: 200,
+    },
+  ];
+}
+
+export function useRoleFormSchema(
+  mode: 'add' | 'edit' | 'setting',
+): VbenFormSchema[] {
+  console.log('useRoleFormSchema', mode);
+  const schema: VbenFormSchema[] = [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.role.name'),
+      rules: 'required',
+    },
+  ];
+
+  // 编辑模式
+  if (mode === 'setting') {
+    schema.push({
+      component: 'Input',
+      fieldName: 'permissions',
+      formItemClass: 'items-start',
+      label: $t('system.role.setPermissions'),
+      modelPropName: 'modelValue',
+      rules: z.array(z.any()).min(1, {
+        message: '请选择菜单权限',
+      }),
+    });
+  }
+
+  // 设置模式
+  if (mode === 'edit') {
+    schema.push({
+      component: 'Input',
+      fieldName: 'remark',
+      label: '备注',
+    });
+  }
+
+  if (mode === 'add') {
+    schema.push({
+      component: 'Input',
+      fieldName: 'remark',
+      label: '备注',
+    });
+  }
+
+  return schema;
+}

+ 203 - 0
apps/smart-pharmacy/src/views/system/role/list.vue

@@ -0,0 +1,203 @@
+<script lang="ts" setup>
+import type { Recordable } from '@vben/types';
+
+import type {
+  OnActionClickParams,
+  VxeTableGridOptions,
+} from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api';
+
+import { Page, useVbenDrawer } from '@vben/common-ui';
+import { Plus } from '@vben/icons';
+
+import { Button, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deleteRoleMethod } from '#/api';
+import { $t } from '#/locales';
+
+import { useRoleSearchFormSchema, useRoleTableColumns } from './data';
+import Form from './modules/form.vue';
+import { mockData } from './mock';
+
+const [FormDrawer, formDrawerApi] = useVbenDrawer({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
+    schema: useRoleSearchFormSchema(),
+    submitOnChange: true,
+  },
+  gridOptions: {
+    columns: useRoleTableColumns(onActionClick, onStatusChange),
+    height: 'auto',
+    keepSource: true,
+    // proxyConfig: {
+    //   ajax: {
+    //     query({ page }, formValues) {
+    //       return listRolesMethod(page.currentPage, page.pageSize, formValues);
+    //     },
+    //   },
+    // },
+    // rowConfig: {
+    //   keyField: 'id',
+    // },
+    proxyConfig: {
+      response: {
+        result: 'Data.Items',
+        total: 'Data.TotalRecordCount',
+      },
+
+      ajax: {
+        query() {
+          return Promise.resolve({
+            Data: mockData.value,
+            ResultInfo: '操作成功',
+            ResultCode: 0,
+          });
+        },
+      },
+    },
+
+    rowConfig: {
+      keyField: 'pid',
+    },
+  } as VxeTableGridOptions<SystemModel.Role>,
+});
+
+function onActionClick(e: OnActionClickParams<SystemModel.Role>) {
+  switch (e.code) {
+    case 'delete': {
+      onDeleteHandle(e.row);
+      break;
+    }
+    case 'edit': {
+      onEditHandle(e.row);
+      break;
+    }
+    case 'set': {
+      onSetHandle(e.row);
+      break;
+    }
+  }
+}
+
+/**
+ * 将Antd的Modal.confirm封装为promise,方便在异步函数中调用。
+ * @param content 提示内容
+ * @param title 提示标题
+ */
+function confirm(content: string, title: string) {
+  return new Promise((reslove, reject) => {
+    Modal.confirm({
+      content,
+      onCancel() {
+        reject(new Error('已取消'));
+      },
+      onOk() {
+        reslove(true);
+      },
+      title,
+    });
+  });
+}
+
+/**
+ * 状态开关即将改变
+ * @param newStatus 期望改变的状态值
+ * @param row 行数据
+ * @returns 返回false则中止改变,返回其他值(undefined、true)则允许改变
+ */
+async function onStatusChange(newStatus: 0 | 1, row: SystemModel.Role) {
+  const status: Recordable<string> = {
+    0: '禁用',
+    1: '启用',
+  };
+  try {
+    await confirm(
+      `你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
+      `切换状态`,
+    );
+    mockData.value.Items = mockData.value.Items.map((item) => {
+      if (item.pid === row.pid) {
+        return {
+          ...item,
+          status: newStatus,
+        };
+      }
+      return item;
+    });
+    // await updateRoleStatusMethod(row.id, { status: newStatus });
+    return true;
+  } catch {
+    return false;
+  }
+}
+
+function onRefresh() {
+  gridApi.query();
+}
+
+function onSetHandle(row?: SystemModel.Role) {
+  // formDrawerApi.setData(row ?? {}).open();
+  formDrawerApi
+    .setData({
+      row,
+      type: 'setting',
+    })
+    .open();
+}
+
+function onEditHandle(row?: SystemModel.Role) {
+  formDrawerApi
+    .setData({
+      row,
+      type: 'edit',
+    })
+    .open();
+  // formDrawerApi.setData(row ?? {}).open();
+}
+function onAddHandle(row?: SystemModel.Role) {
+  formDrawerApi
+    .setData({
+      row,
+      type: 'add',
+    })
+    .open();
+  // formDrawerApi.setData(row ?? {}).open();
+}
+
+async function onDeleteHandle(row: SystemModel.Role) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.name]),
+    duration: 0,
+    key: 'action_process_msg',
+  });
+  try {
+    await deleteRoleMethod(row);
+    message.success({
+      content: $t('ui.actionMessage.deleteSuccess', [row.name]),
+      key: 'action_process_msg',
+    });
+    onRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+</script>
+<template>
+  <Page auto-content-height>
+    <FormDrawer @success="onRefresh" />
+    <Grid>
+      <template #toolbar-tools>
+        <Button type="primary" @click="onAddHandle()">
+          <Plus class="size-5" />
+          {{ $t('ui.actionTitle.create', [$t('system.role._')]) }}
+        </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 35 - 0
apps/smart-pharmacy/src/views/system/role/mock.ts

@@ -0,0 +1,35 @@
+import { ref } from 'vue';
+
+export const mockData = ref({
+  TotalRecordCount: 11,
+  TotalPageCount: 1,
+  PageSize: 20,
+  ButtonRight: '',
+  CurrentPageSize: 11,
+  Items: [
+    {
+      pid: '760631a1-58ca-49e7-9ec9-82475dbbcb5c',
+      createUser: 'admin',
+      lastTime: '2025-10-26 11:25:12',
+      isDelete: '0',
+      params: {},
+      name: '科长',
+      stateSel: '0',
+      status: 1,
+      remark: '这是一个备注222',
+      roleCode: '65015601',
+    },
+    {
+      pid: 'c0c6cd02-2ea9-4ec8-86e4-71239fe9f156',
+      createUser: 'admin',
+      lastTime: '2025-10-22 09:22:05',
+      isDelete: '0',
+      params: {},
+      name: '管理员222',
+      stateSel: '0',
+      status: 0,
+      remark: '这是一个备注',
+      roleCode: '65015600',
+    },
+  ],
+});

+ 251 - 0
apps/smart-pharmacy/src/views/system/role/modules/form.vue

@@ -0,0 +1,251 @@
+<script lang="ts" setup>
+import type { Recordable } from '@vben/types';
+
+import type { SystemModel } from '#/api';
+
+import { computed, ref } from 'vue';
+
+import { useVbenDrawer, VbenTree } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { useRequest } from '@six/request';
+import { Spin } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { editRoleMethod, getMenusMethod } from '#/api';
+import { $t } from '#/locales';
+
+import { useRoleFormSchema } from '../data';
+
+import { mockData } from '../mock';
+
+const emits = defineEmits(['success']);
+
+// const {
+//   loading,
+//   data: menus,
+//   send: loadMenus,
+// } = useRequest(getMenusMethod, { immediate: false, initialData: [] });
+
+const loading = ref(false);
+const menus = [
+  {
+    meta: {
+      icon: 'ion:settings-outline',
+      order: 9997,
+      title: 'system.title',
+    },
+    name: 'System',
+    path: '/system',
+    children: [
+      {
+        path: '/system/organization',
+        name: 'MedicalOrganization',
+        meta: {
+          icon: 'mdi:account-group',
+          title: '医疗机构管理',
+        },
+        component: '/system/organization/list',
+      },
+      {
+        path: '/system/enterprise',
+        name: 'SystemEnterprise',
+        meta: {
+          icon: 'mdi:account-group',
+          title: '企业管理',
+        },
+        component: '/system/enterprise/list',
+      },
+      {
+        path: '/system/user',
+        name: 'SystemUser',
+        meta: {
+          icon: 'charm:organisation',
+          title: '煎药中心管理',
+        },
+        component: '/system/user/list',
+      },
+      {
+        path: '/system/role',
+        name: 'SystemRole',
+        meta: {
+          icon: 'charm:organisation',
+          title: '角色管理',
+        },
+        component: '/system/role/list',
+      },
+      {
+        path: '/system/project',
+        name: 'SystemProject',
+        meta: {
+          icon: 'charm:organisation',
+          title: '用户管理',
+        },
+        component: '/system/project/list',
+      },
+    ],
+  },
+  {
+    meta: {
+      icon: 'ion:settings-outline',
+      order: 9998,
+      title: '处方管理',
+    },
+    name: 'RegisterRegister',
+    path: '/register/register',
+    children: [
+      {
+        path: '/system/role',
+        name: 'SystemRole1',
+        meta: {
+          icon: 'mdi:account-group',
+          title: '处方列表',
+        },
+        component: '/system/role/list',
+      },
+    ],
+  },
+];
+
+const edit = useRequest(editRoleMethod, { immediate: false }).onSuccess(() => {
+  emits('success');
+});
+
+const formData = ref<SystemModel.Role>();
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', [$t('system.role.name')])
+    : $t('ui.actionTitle.create', [$t('system.role.name')]);
+});
+
+const title = ref('新增');
+const setTitle = computed(() => {
+  if (title.value === 'setting') {
+    return '角色权限设置';
+  }
+  if (title.value === 'edit') {
+    return $t('ui.actionTitle.edit', [$t('system.role.name')]);
+  }
+  return $t('ui.actionTitle.create', [$t('system.role.name')]);
+});
+
+const mode = ref<'add' | 'edit' | 'setting'>('add');
+
+const [Form, formApi] = useVbenForm({
+  schema: [],
+  showDefaultActions: false,
+});
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  async onConfirm() {
+    mockData.value.Items.push({
+      id: String(Math.random()),
+      name: formData.value?.name || '',
+      code: 'new_role_code',
+      createUser: 'admin',
+      lastTime: new Date().toISOString(),
+    });
+    drawerApi.close();
+    // const { valid } = await formApi.validate();
+    // if (!valid) return;
+    // drawerApi.lock();
+    // const data = await formApi.getValues();
+    // try {
+    //   await edit.send({ ...formData.value, ...data });
+    //   await drawerApi.close();
+    // } finally {
+    //   drawerApi.unlock();
+    // }
+  },
+
+  async onOpenChange(isOpen) {
+    if (isOpen) {
+      // const data = drawerApi.getData<SystemModel.Role>();
+      const data = drawerApi.getData<{
+        row?: SystemModel.Role;
+        type: 'add' | 'edit' | 'setting';
+      }>();
+      await formApi.resetForm();
+      if (data) {
+        // formData.value = data;
+        formData.value = data.row;
+        title.value = data.type;
+        mode.value = data.type;
+        await formApi.setState({
+          schema: useRoleFormSchema(mode.value),
+        });
+        await formApi.setValues(formData.value);
+        // 清除验证状态,避免在数据设置后立即显示验证错误
+        await formApi.resetValidate();
+      }
+      if (menus.value.length === 0) {
+        await loadMenus();
+        if (formData.value?.permissions?.length) {
+          await formApi.setValues(formData.value);
+          // 再次清除验证状态
+          await formApi.resetValidate();
+        }
+      }
+    }
+  },
+});
+
+function getNodeClass(node: Recordable<any>) {
+  const classes: string[] = [];
+  if (node.value?.type === 'button') {
+    classes.push('inline-flex');
+    if (node.index % 3 >= 1) {
+      classes.push('!pl-0');
+    }
+  }
+
+  return classes.join(' ');
+}
+</script>
+<template>
+  <Drawer :title="setTitle">
+    <Form>
+      <template #permissions="slotProps">
+        <Spin :spinning="loading" wrapper-class-name="w-full menus-loading">
+          <VbenTree
+            :tree-data="menus"
+            multiple
+            bordered
+            :default-expanded-level="2"
+            :get-node-class="getNodeClass"
+            v-bind="slotProps"
+            value-field="id"
+            label-field="meta.title"
+            icon-field="meta.icon"
+          >
+            <template #node="{ value }">
+              <IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" />
+              {{ $t(value.meta.title) }}
+            </template>
+          </VbenTree>
+        </Spin>
+      </template>
+    </Form>
+  </Drawer>
+</template>
+<style lang="css" scoped>
+.menus-loading {
+  min-height: 24px;
+}
+
+:deep(.ant-tree-title) {
+  .tree-actions {
+    display: none;
+    margin-left: 20px;
+  }
+}
+
+:deep(.ant-tree-title:hover) {
+  .tree-actions {
+    display: flex;
+    flex: auto;
+    justify-content: flex-end;
+    margin-left: 20px;
+  }
+}
+</style>

+ 108 - 0
apps/smart-pharmacy/src/views/system/tisane/data.ts

@@ -0,0 +1,108 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api/method/system';
+
+import { listOrganizationsMethodAll } from '#/api/method/system';
+import { $t } from '#/locales';
+
+export function useUserSearchFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.tisane.name'),
+    },
+    {
+      component: 'Select',
+      fieldName: 'status',
+      label: $t('system.tisane.relatedEnterprise'),
+      componentProps: {
+        options: [
+          {
+            label: '医疗机构1',
+            value: 1,
+          },
+          {
+            label: '医疗机构2',
+            value: 0,
+          },
+          {
+            label: '医疗机构3',
+            value: 2,
+          },
+        ],
+      },
+    },
+  ];
+}
+
+export function useUserTableColumns<T = SystemModel.Organization>(
+  onActionClick?: OnActionClickFn<T>,
+): VxeTableGridOptions<T>['columns'] {
+  return [
+    { type: 'seq', title: $t('table.column.seq'), width: 50 },
+    {
+      field: 'name',
+      title: $t('system.tisane.name'),
+      minWidth: 100,
+    },
+    {
+      field: 'code',
+      title: $t('system.tisane.code'),
+      minWidth: 100,
+    },
+    {
+      field: 'type',
+      title: $t('system.tisane.type'),
+      minWidth: 100,
+    },
+    {
+      field: 'relatedOrganizations',
+      title: $t('system.tisane.relatedOrganization'),
+      minWidth: 100,
+    },
+    {
+      field: 'relatedEnterprise',
+      title: $t('system.tisane.relatedEnterprise'),
+      minWidth: 100,
+    },
+    {
+      field: 'remark',
+      title: $t('system.tisane.remark'),
+      minWidth: 100,
+    },
+    {
+      field: 'createTime',
+      title: $t('system.tisane.createTime'),
+      minWidth: 100,
+    },
+    {
+      field: 'createUser',
+      title: $t('system.tisane.createUser'),
+      minWidth: 100,
+    },
+    {
+      align: 'center',
+      // cellRender: {
+      //   attrs: {
+      //     nameField: 'name',
+      //     nameTitle: $t('system.user._'),
+      //     onClick: onActionClick,
+      //   },
+      //   name: 'CellOperation',
+      // },
+      field: 'operation',
+      fixed: 'right',
+      title: $t('table.column.operation'),
+      width: 130,
+    },
+  ];
+}
+
+export function useUserFormSchema(
+  current?: Pick<SystemModel.Organization, 'id' | 'name'>,
+): VbenFormSchema[] {
+  return [];
+}

+ 114 - 0
apps/smart-pharmacy/src/views/system/tisane/list.vue

@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+
+import { useUserSearchFormSchema, useUserTableColumns } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useUserSearchFormSchema(),
+    submitOnChange: true,
+  },
+  gridOptions: {
+    columns: useUserTableColumns(),
+    height: 'auto',
+    keepSource: true,
+    // proxyConfig: {
+    //   ajax: {
+    //     query({ page }, formValues) {
+    //       return listOrganizationsMethod(
+    //         page.currentPage,
+    //         page.pageSize,
+    //         formValues,
+    //       );
+    //     },
+    //   },
+    // },
+    // rowConfig: {
+    //   keyField: 'id',
+    // },
+
+    proxyConfig: {
+      ajax: {
+        query() {
+          return Promise.resolve({
+            Data: {
+              TotalRecordCount: 3,
+
+              Items: [
+                {
+                  pid: '1',
+                  name: '重药煎药中心华东区',
+                  code: 'cyeast',
+                  createUser: 'createUser',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '企业煎药中心',
+                  remark: '备注1',
+                  relatedOrganizations: '医疗机构1',
+                  relatedEnterprise: '浙江药业公司',
+                },
+                {
+                  pid: '2',
+                  name: '煎药中心1',
+                  code: 'jyzhongxin1',
+                  createUser: 'createUser',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '第三方煎药中心',
+                  remark: '备注2',
+                  relatedOrganizations: '医疗机构2',
+                  relatedEnterprise: '重药控股有限公司',
+                },
+                {
+                  pid: '3',
+                  name: '煎药中心2',
+                  code: 'jyzhongxin2',
+                  createUser: 'createUser',
+                  createTime: '2025-10-26 11:23:21',
+                  type: '第三方煎药中心',
+                  remark: '备注333',
+                  relatedOrganizations: '医疗机构3',
+                  relatedEnterprise: '重药控股有限公司',
+                },
+              ],
+            },
+
+            ResultCode: 0,
+          });
+        },
+      },
+
+      response: {
+        result: 'Data.Items',
+        total: 'Data.TotalRecordCount',
+      },
+    },
+
+    rowConfig: {
+      keyField: 'pid',
+    },
+  } as VxeTableGridOptions<SystemModel.User>,
+});
+
+// 刷新
+function onRefresh() {
+  gridApi.query();
+}
+</script>
+<template>
+  <Page auto-content-height>
+    <FormModal @success="onRefresh" />
+    <Grid>
+      <template #toolbar-tools></template>
+    </Grid>
+  </Page>
+</template>

+ 49 - 0
apps/smart-pharmacy/src/views/system/tisane/modules/form.vue

@@ -0,0 +1,49 @@
+<script lang="ts" setup>
+import type { SystemModel } from '#/api';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { $t } from '#/locales';
+
+import { useUserFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const formData = ref<SystemModel.Organization>();
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', [$t('system.organization._')])
+    : $t('ui.actionTitle.create', [$t('system.organization._')]);
+});
+
+const [Form, formApi] = useVbenForm({
+  // layout: 'vertical',
+  schema: useUserFormSchema(),
+  showDefaultActions: false,
+});
+
+function resetForm() {
+  formApi.resetForm();
+  formApi.setValues(formData.value || {});
+}
+
+const [Modal, modalApi] = useVbenModal({});
+</script>
+
+<template>
+  <Modal :title="getTitle">
+    <Form class="mx-4" />
+    <template #prepend-footer>
+      <div class="flex-auto">
+        <Button type="primary" danger @click="resetForm">
+          {{ $t('common.reset') }}
+        </Button>
+      </div>
+    </template>
+  </Modal>
+</template>

+ 229 - 0
apps/smart-pharmacy/src/views/system/user/data.ts

@@ -0,0 +1,229 @@
+import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
+
+import type { VbenFormSchema } from '#/adapter/form';
+import type { OnActionClickFn } from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api/method/system';
+
+import { z } from '#/adapter/form';
+import {
+  listUsersInstitutionMethodTree,
+  optionsRoleMethod,
+} from '#/api/method/system';
+import { $t } from '#/locales';
+import { mockData } from './mock';
+
+export function useUserSearchFormSchema(): VbenFormSchema[] {
+  return [
+    { component: 'Input', fieldName: 'name', label: $t('system.role.name') },
+    {
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: $t('common.all'), value: null },
+          { label: $t('common.enabled'), value: 0 },
+          { label: $t('common.disabled'), value: 1 },
+        ],
+      },
+      fieldName: 'status',
+      label: $t('system.role.status'),
+    },
+  ];
+}
+
+export function useUserTableColumns<T = SystemModel.User>(
+  onActionClick?: OnActionClickFn<T>,
+  onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
+): VxeTableGridOptions<T>['columns'] {
+  return [
+    {
+      field: 'access',
+      title: $t('system.user.access'),
+      minWidth: 100,
+    },
+    {
+      field: 'name',
+      title: $t('system.user.name'),
+      minWidth: 100,
+    },
+    {
+      field: 'hospitalName',
+      title: $t('system.organization.belong'),
+      minWidth: 100,
+    },
+    {
+      field: 'phone',
+      title: $t('system.organization.phone'),
+      minWidth: 100,
+    },
+    // {
+    //   field: 'rolename',
+    //   title: $t('system.role._'),
+    //   minWidth: 100,
+    // },
+    {
+      field: 'rolename',
+      title: $t('system.role._'),
+      minWidth: 150,
+      slots: {
+        default: ({ row }) => {
+          // 使用 map 提取所有 rolename,然后用逗号连接
+          const roleNames =
+            row.roles
+              ?.map((item: any) => item?.rolename?.trim?.())
+              .filter(Boolean) || [];
+
+          return roleNames.join(',') || '-';
+        },
+      },
+    },
+    {
+      field: 'createUser',
+      title: $t('system.organization.createUser'),
+      minWidth: 100,
+    },
+    {
+      field: 'createTime',
+      title: $t('table.column.createTime'),
+      minWidth: 100,
+    },
+    {
+      cellRender: {
+        attrs: { beforeChange: onStatusChange },
+        // props: { accessRole: '超级管理员' },
+        props: { _props: { checkedValue: 0, unCheckedValue: 1 } },
+        name: onStatusChange ? 'CellSwitch' : 'CellTag',
+      },
+      field: 'status',
+      title: $t('system.role.enabledStatus'),
+      width: 100,
+    },
+    {
+      align: 'center',
+      cellRender: {
+        attrs: {
+          nameField: 'name',
+          nameTitle: $t('system.user._'),
+          onClick: onActionClick,
+        },
+        name: 'CellOperation',
+      },
+      field: 'operation',
+      fixed: 'right',
+      title: $t('table.column.operation'),
+      width: 130,
+    },
+  ];
+}
+
+export function useUserFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'ApiTreeSelect',
+      componentProps: {
+        class: 'w-full',
+        // api: listUsersInstitutionMethodTree,
+        options: [
+          {
+            label: '京慈堂杭州分堂',
+            value: 1,
+          },
+          {
+            label: '京慈堂',
+            value: 2,
+          },
+          {
+            label: '测试机构1',
+            value: 3,
+          },
+        ],
+        labelField: 'name',
+        valueField: 'pid',
+        childrenField: 'children',
+        treeDefaultExpandAll: true,
+        dropdownStyle: { maxHeight: 400, overflow: 'auto' },
+      },
+      fieldName: 'hospitalName',
+      label: $t('system.organization.belong'),
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      fieldName: 'access',
+      label: $t('system.user.access'),
+      rules: 'required',
+    },
+    {
+      component: 'InputPassword',
+      componentProps: {
+        placeholder: '请输入密码',
+      },
+      fieldName: 'password',
+      label: '密码',
+      rules: z
+        .string()
+        .refine(
+          (value) =>
+            /^(?=.*[A-Z])(?=.*[a-z]|.*\d)|(?=.*[a-z])(?=.*[A-Z]|.*\d)|(?=.*\d)(?=.*[A-Z]|.*[a-z]).{8,18}$/.test(
+              value,
+            ),
+          { message: '请输入8-18位数字、大写字母、小写字母最少两种组合的密码' },
+        ),
+    },
+    {
+      component: 'Input',
+      fieldName: 'name',
+      label: $t('system.user.name'),
+      rules: 'required',
+    },
+    {
+      component: 'Input',
+      fieldName: 'phone',
+      label: $t('system.user.mobile'),
+      rules: z
+        .string()
+        .length(11, $t('ui.formRules.length', [$t('system.user.mobile'), 11])),
+    },
+    {
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        // api: optionsRoleMethod,
+        options: [
+          {
+            label: '科长',
+            value: '760631a1-58ca-49e7-9ec9-82475dbbcb5c',
+          },
+          {
+            label: '治疗师',
+            value: "ee272511-ecd9-45f7-a405-50c58395629d",
+          },
+          {
+            label: '账号',
+            value: "8e317a00-d83e-40ed-b1ba-b7d5a9757cbb",
+          },
+          {
+            label: '系统管理',
+            value: "53c0c7dc-3cd9-4b35-988e-2d6d57715787",
+          },
+        ],
+        class: 'w-full',
+        // labelField: 'name',
+        // valueField: 'id',
+        childrenField: 'children',
+        mode: 'multiple',
+      },
+      fieldName: 'roles',
+      // fieldName: 'rolesNew',
+      label: $t('system.role._'),
+      rules: 'selectRequired',
+    },
+
+    {
+      component: 'Input',
+      fieldName: 'remark',
+      label: $t('system.user.remark'),
+      rules: '',
+    },
+  ];
+}

+ 186 - 0
apps/smart-pharmacy/src/views/system/user/list.vue

@@ -0,0 +1,186 @@
+<script lang="ts" setup>
+import type { Recordable } from '@vben/types';
+
+import type {
+  OnActionClickParams,
+  VxeTableGridOptions,
+} from '#/adapter/vxe-table';
+import type { SystemModel } from '#/api';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { Plus } from '@vben/icons';
+
+import { Button, message, Modal, notification } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { $t } from '#/locales';
+
+import { useUserSearchFormSchema, useUserTableColumns } from './data';
+import { mockData } from './mock';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useUserSearchFormSchema(),
+    submitOnChange: true,
+  },
+  gridOptions: {
+    columns: useUserTableColumns(onActionClick, onStatusChange),
+    height: 'auto',
+    keepSource: true,
+    // proxyConfig: {
+    //   ajax: {
+    //     query({ page }, formValues) {
+    //       return listUsersMethod(page.currentPage, page.pageSize, formValues);
+    //     },
+    //   },
+    // },
+    // rowConfig: {
+    //   keyField: 'id',
+    // },
+    proxyConfig: {
+      response: {
+        result: 'Data.Items',
+        total: 'Data.TotalRecordCount',
+      },
+
+      ajax: {
+        query() {
+          return Promise.resolve({
+            Data: mockData,
+            ResultInfo: '操作成功',
+            ResultCode: 0,
+          });
+        },
+      },
+    },
+
+    rowConfig: {
+      keyField: 'pid',
+    },
+  } as VxeTableGridOptions<SystemModel.User>,
+});
+
+function onActionClick(e: OnActionClickParams<SystemModel.User>) {
+  switch (e.code) {
+    case 'delete': {
+      onDeleteHandle(e.row);
+      break;
+    }
+    case 'edit': {
+      onEditHandle(e.row);
+      break;
+    }
+  }
+}
+
+/**
+ * 将Antd的Modal.confirm封装为promise,方便在异步函数中调用。
+ * @param content 提示内容
+ * @param title 提示标题
+ */
+function confirm(content: string, title: string) {
+  return new Promise((reslove, reject) => {
+    Modal.confirm({
+      content,
+      onCancel() {
+        reject(new Error('已取消'));
+      },
+      onOk() {
+        reslove(true);
+      },
+      title,
+    });
+  });
+}
+
+/**
+ * 状态开关即将改变
+ * @param newStatus 期望改变的状态值
+ * @param row 行数据
+ * @returns 返回false则中止改变,返回其他值(undefined、true)则允许改变
+ */
+async function onStatusChange(newStatus: 0 | 1, row: SystemModel.User) {
+  const status: Recordable<string> = {
+    0: '启用',
+    1: '禁用',
+  };
+  try {
+    await confirm(
+      `你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
+      `切换状态`,
+    );
+
+    try {
+      // await updateUserStatusMethod(row.pid!, { status: newStatus });
+      mockData.Items = mockData.Items.map((item) => {
+        if (item.pid === row.pid) {
+          return {
+            ...item,
+            status: newStatus,
+          };
+        }
+        return item;
+      });
+      notification.success({
+        message: '切换状态成功',
+      });
+    } catch (error: any) {
+      notification.error({
+        message: error.message || '切换状态失败',
+      });
+    }
+
+    return true;
+  } catch {
+    return false;
+  }
+}
+
+function onRefresh() {
+  gridApi.query();
+}
+
+function onEditHandle(row?: SystemModel.User) {
+  formModalApi.setData(row ?? {}).open();
+  console.log('row', row);
+  console.log('mockdata', mockData)
+}
+
+async function onDeleteHandle(row: SystemModel.User) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.name]),
+    duration: 0,
+    key: 'action_process_msg',
+  });
+  try {
+    console.log('删除', row);;
+    // await deleteUserMethod(row);
+    message.success({
+      content: $t('ui.actionMessage.deleteSuccess', [row.name]),
+      key: 'action_process_msg',
+    });
+    onRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+</script>
+<template>
+  <Page auto-content-height>
+    <FormModal @success="onRefresh" />
+    <Grid>
+      <template #toolbar-tools>
+        <Button type="primary" @click="onEditHandle()">
+          <Plus class="size-5" />
+          {{ $t('ui.actionTitle.create', [$t('system.user._')]) }}
+        </Button>
+      </template>
+    </Grid>
+  </Page>
+</template>

+ 154 - 0
apps/smart-pharmacy/src/views/system/user/mock.ts

@@ -0,0 +1,154 @@
+export const mockData =
+{
+  TotalRecordCount: 9,
+  TotalPageCount: 1,
+  PageSize: 20,
+  ButtonRight: '',
+  CurrentPageSize: 9,
+  Items: [
+    {
+      pid: '11ba437b-2a37-4383-89e5-304ac94b12a7',
+      access: 'kezhang',
+      name: '张三',
+      phone: '13456789654',
+      password: '$2a10YyzC0Uqtt/U749Sw9HiCfeFrgEpzSFRoZSk1LPirpQLEL6q5UJwrC',
+      stateSel: '0',
+      status: 0,
+      rolesNew:[1],
+      roles: [
+        {
+          key: null,
+          pid: '760631a1-58ca-49e7-9ec9-82475dbbcb5c',
+          groupId: null,
+          searchValue: null,
+          createUser: 'admin',
+          createTime: '2025-10-26 11:25:12',
+          updateUser: null,
+          updateTime: null,
+          isDelete: '0',
+          params: {},
+          roleid: null,
+          rolename: '科长',
+          englishName: null,
+          roleSort: null,
+          stateSel: '0',
+          rolecode: null,
+          menuIds: null,
+          remark: null,
+        },
+      ],
+      roleIds: null,
+      hospitalName: '京慈堂杭州分堂',
+      sititutionId: '1cb7159d-b9e4-462f-a12b-eaa7b27d555b',
+      createUser: 'admin',
+      createTime: '2025-10-26 11:26:56',
+      updateUser: null,
+      updateTime: null,
+      isDelete: '0',
+      jobnumber: 'jc003',
+      remark: '备注1',
+    },
+    {
+      pid: '8f364111-b766-448b-909a-04eb041192e5',
+      access: 'jc2',
+      name: 'jc治疗师',
+      phone: '13678954678',
+      password: '$2a10x0MBhHwwaBj0AFXzdqv8GOu6pA8UXvsg9hBlLvSMJR83G69Lrs6Z2',
+      stateSel: '0',
+      rolesNew:[2],
+      roles: [
+        {
+          key: null,
+          pid: 'ee272511-ecd9-45f7-a405-50c58395629d',
+          groupId: null,
+          searchValue: null,
+          createUser: '京慈管理员',
+          createTime: '2025-10-17 18:01:01',
+          updateUser: null,
+          updateTime: null,
+          isDelete: '0',
+          params: {},
+          roleid: null,
+          rolename: '治疗师',
+          englishName: null,
+          roleSort: null,
+          stateSel: '0',
+          rolecode: null,
+          menuIds: null,
+          remark: null,
+        },
+      ],
+      roleIds: null,
+      hospitalName: '京慈堂',
+      sititutionId: 'e9597287-6e45-43b1-b9bd-91ab2884d2eb',
+      createUser: '京慈管理员',
+      createTime: '2025-10-17 18:02:59',
+      updateUser: 'admin',
+      updateTime: '2025-10-22 14:29:52',
+      isDelete: '0',
+      jobnumber: '002',
+      remark: '备注2',
+    },
+    {
+      pid: '70de1ca3-e58e-4ad6-b184-62bc2cbef86a',
+      access: 'ztadmin',
+      name: '测试',
+      phone: '13781574172',
+      password: '$2a10ms57FJBUmjirGYKrsFhgj.6nz6HERecZQoHc5Nyul4yLx0OqbxOWe',
+      stateSel: '0',
+      rolesNew:[4,3],
+      roles: [
+        {
+          key: null,
+          pid: '8e317a00-d83e-40ed-b1ba-b7d5a9757cbb',
+          groupId: null,
+          searchValue: null,
+          createUser: 'admin',
+          createTime: '2025-09-09 15:31:27',
+          updateUser: null,
+          updateTime: '2025-09-09 17:36:06',
+          isDelete: '0',
+          params: {},
+          roleid: null,
+          rolename: '账号',
+          englishName: null,
+          roleSort: null,
+          stateSel: '0',
+          rolecode: null,
+          menuIds: null,
+          remark: '账号管理员(账号管理)',
+        },
+        {
+          key: null,
+          pid: '53c0c7dc-3cd9-4b35-988e-2d6d57715787',
+          groupId: null,
+          searchValue: null,
+          createUser: 'admin',
+          createTime: '2025-09-09 15:38:19',
+          updateUser: null,
+          updateTime: '2025-09-30 16:07:50',
+          isDelete: '0',
+          params: {},
+          roleid: null,
+          rolename: '系统管理',
+          englishName: null,
+          roleSort: null,
+          stateSel: '0',
+          rolecode: null,
+          menuIds: null,
+          remark: '系统管理员(账号 + 角色)',
+        },
+      ],
+      roleIds: null,
+      hospitalName: '测试机构1',
+      sititutionId: '1',
+      createUser: '1',
+      createTime: '2025-09-30 16:10:30',
+      updateUser: null,
+      updateTime: null,
+      isDelete: '0',
+      jobnumber: '9999',
+      remark: '备注3',
+    },
+  ],
+}

+ 91 - 0
apps/smart-pharmacy/src/views/system/user/modules/form.vue

@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import type { SystemModel } from '#/api';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { useRequest } from '@six/request';
+import { Button } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { editUserMethod } from '#/api';
+import { $t } from '#/locales';
+
+import { useUserFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const edit = useRequest(editUserMethod, { immediate: false }).onSuccess(() => {
+  emit('success');
+});
+
+const formData = ref<SystemModel.User>();
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', [$t('system.user._')])
+    : $t('ui.actionTitle.create', [$t('system.user._')]);
+});
+
+const [Form, formApi] = useVbenForm({
+  // layout: 'vertical',
+  schema: useUserFormSchema(),
+  showDefaultActions: false,
+});
+
+function resetForm() {
+  formApi.resetForm();
+  formApi.setValues(formData.value || {});
+}
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (valid) {
+      modalApi.lock();
+      const data = await formApi.getValues();
+      try {
+        console.log('data', data)
+        // await edit.send({ ...formData.value, ...data });
+        await modalApi.close();
+      } finally {
+        modalApi.lock(false);
+      }
+    }
+  },
+  onOpenChange(isOpen) {
+    if (isOpen) {
+      const data = modalApi.getData<SystemModel.User>();
+      if (data) {
+        if (data.id) {
+          formApi.setState((prev) => {
+            const schema =
+              prev.schema?.filter(
+                (schema) => schema.fieldName !== 'password',
+              ) ?? [];
+            return { schema };
+          });
+        }
+        formData.value = data;
+        formData.value.roles = data.roles?.map((role) =>
+          typeof role === 'string' ? role : role.pid,
+        );
+        formApi.setValues(formData.value);
+      }
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle">
+    <Form class="mx-4" />
+    <template #prepend-footer>
+      <div class="flex-auto">
+        <Button type="primary" danger @click="resetForm">
+          {{ $t('common.reset') }}
+        </Button>
+      </div>
+    </template>
+  </Modal>
+</template>

+ 1 - 0
apps/smart-pharmacy/tailwind.config.mjs

@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config';

+ 12 - 0
apps/smart-pharmacy/tsconfig.json

@@ -0,0 +1,12 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/tsconfig/web-app.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "#/*": ["./src/*"]
+    }
+  },
+  "references": [{ "path": "./tsconfig.node.json" }],
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}

+ 10 - 0
apps/smart-pharmacy/tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@vben/tsconfig/node.json",
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "noEmit": false
+  },
+  "include": ["vite.config.mts"]
+}

+ 25 - 0
apps/smart-pharmacy/vite.config.mts

@@ -0,0 +1,25 @@
+import { defineConfig } from '@vben/vite-config';
+
+export default defineConfig(async () => {
+  return {
+    application: {},
+    vite: {
+      server: {
+        proxy: {
+          '/wf': {
+            changeOrigin: true,
+            target: `https://wx.hzliuzhi.com:4433`,
+            ws: true,
+          },
+          '/api': {
+            changeOrigin: true,
+            rewrite: (path) => path.replace(/^\/api/, ''),
+            // mock代理目标地址
+            target: 'http://192.168.1.16:8039',
+            ws: true,
+          },
+        },
+      },
+    },
+  };
+});