gugu 2 mēneši atpakaļ
revīzija
e72cecea32
100 mainītis faili ar 12668 papildinājumiem un 0 dzēšanām
  1. 39 0
      .gitignore
  2. 8 0
      .idea/.gitignore
  3. 5 0
      .idea/codeStyles/codeStyleConfig.xml
  4. 7 0
      .idea/encodings.xml
  5. 15 0
      .idea/misc.xml
  6. 6 0
      .idea/vcs.xml
  7. 4 0
      11.txt
  8. 24 0
      frontend/.gitignore
  9. 5 0
      frontend/README.md
  10. 13 0
      frontend/index.html
  11. 1850 0
      frontend/package-lock.json
  12. 22 0
      frontend/package.json
  13. BIN
      frontend/public/img/52a0a789-b7af-4b73-bc8e-85ca74f80965.jpg
  14. BIN
      frontend/public/img/b223d09a-5649-4793-b316-7473bb78eb90.jpg
  15. 1 0
      frontend/public/vite.svg
  16. 9 0
      frontend/src/App.vue
  17. 1 0
      frontend/src/assets/vue.svg
  18. 43 0
      frontend/src/components/HelloWorld.vue
  19. 8 0
      frontend/src/main.js
  20. 69 0
      frontend/src/router/index.js
  21. 38 0
      frontend/src/style.css
  22. 40 0
      frontend/src/utils/request.js
  23. 230 0
      frontend/src/views/Layout.vue
  24. 253 0
      frontend/src/views/Login.vue
  25. 1007 0
      frontend/src/views/device/Index.vue
  26. 820 0
      frontend/src/views/hospital/Bed.vue
  27. 561 0
      frontend/src/views/hospital/Department.vue
  28. 446 0
      frontend/src/views/hospital/Overview.vue
  29. 813 0
      frontend/src/views/hospital/Room.vue
  30. 43 0
      frontend/src/views/hospital/Ward.vue
  31. 726 0
      frontend/src/views/hospital/WardArea.vue
  32. 1328 0
      frontend/src/views/patient/Index.vue
  33. 22 0
      frontend/vite.config.js
  34. 6 0
      package-lock.json
  35. 102 0
      pom.xml
  36. 13 0
      src/main/java/org/example/Main.java
  37. 47 0
      src/main/java/org/example/config/CacheConstants.java
  38. 32 0
      src/main/java/org/example/config/CorsConfig.java
  39. 71 0
      src/main/java/org/example/config/RedisConfig.java
  40. 23 0
      src/main/java/org/example/config/WebConfig.java
  41. 22 0
      src/main/java/org/example/controller/BaseController.java
  42. 185 0
      src/main/java/org/example/controller/BedController.java
  43. 91 0
      src/main/java/org/example/controller/DepartmentController.java
  44. 110 0
      src/main/java/org/example/controller/HospitalController.java
  45. 38 0
      src/main/java/org/example/controller/LoginController.java
  46. 336 0
      src/main/java/org/example/controller/PatientController.java
  47. 173 0
      src/main/java/org/example/controller/RoomController.java
  48. 181 0
      src/main/java/org/example/controller/TerminalController.java
  49. 172 0
      src/main/java/org/example/controller/WardAreaController.java
  50. 26 0
      src/main/java/org/example/entity/Bed.java
  51. 22 0
      src/main/java/org/example/entity/Department.java
  52. 18 0
      src/main/java/org/example/entity/Hospital.java
  53. 107 0
      src/main/java/org/example/entity/PageResult.java
  54. 38 0
      src/main/java/org/example/entity/Patient.java
  55. 26 0
      src/main/java/org/example/entity/PatientCharge.java
  56. 23 0
      src/main/java/org/example/entity/PatientDiagnosis.java
  57. 28 0
      src/main/java/org/example/entity/PatientExam.java
  58. 31 0
      src/main/java/org/example/entity/PatientOrder.java
  59. 27 0
      src/main/java/org/example/entity/PatientTest.java
  60. 22 0
      src/main/java/org/example/entity/PatientTestItem.java
  61. 22 0
      src/main/java/org/example/entity/Room.java
  62. 24 0
      src/main/java/org/example/entity/Terminal.java
  63. 14 0
      src/main/java/org/example/entity/Users.java
  64. 26 0
      src/main/java/org/example/entity/WardArea.java
  65. 15 0
      src/main/java/org/example/entity/Yonghu.java
  66. 67 0
      src/main/java/org/example/mapper/BedMapper.java
  67. 39 0
      src/main/java/org/example/mapper/DepartmentMapper.java
  68. 27 0
      src/main/java/org/example/mapper/HospitalMapper.java
  69. 39 0
      src/main/java/org/example/mapper/PatientChargeMapper.java
  70. 44 0
      src/main/java/org/example/mapper/PatientDiagnosisMapper.java
  71. 39 0
      src/main/java/org/example/mapper/PatientExamMapper.java
  72. 79 0
      src/main/java/org/example/mapper/PatientMapper.java
  73. 39 0
      src/main/java/org/example/mapper/PatientOrderMapper.java
  74. 24 0
      src/main/java/org/example/mapper/PatientTestItemMapper.java
  75. 44 0
      src/main/java/org/example/mapper/PatientTestMapper.java
  76. 62 0
      src/main/java/org/example/mapper/RoomMapper.java
  77. 58 0
      src/main/java/org/example/mapper/TerminalMapper.java
  78. 17 0
      src/main/java/org/example/mapper/UsersMapper.java
  79. 56 0
      src/main/java/org/example/mapper/WardAreaMapper.java
  80. 17 0
      src/main/java/org/example/mapper/YonghuMapper.java
  81. 146 0
      src/main/java/org/example/service/BedService.java
  82. 61 0
      src/main/java/org/example/service/DepartmentService.java
  83. 41 0
      src/main/java/org/example/service/HospitalService.java
  84. 60 0
      src/main/java/org/example/service/LoginService.java
  85. 50 0
      src/main/java/org/example/service/PatientChargeService.java
  86. 57 0
      src/main/java/org/example/service/PatientDiagnosisService.java
  87. 50 0
      src/main/java/org/example/service/PatientExamService.java
  88. 50 0
      src/main/java/org/example/service/PatientOrderService.java
  89. 128 0
      src/main/java/org/example/service/PatientService.java
  90. 32 0
      src/main/java/org/example/service/PatientTestItemService.java
  91. 57 0
      src/main/java/org/example/service/PatientTestService.java
  92. 138 0
      src/main/java/org/example/service/RoomService.java
  93. 138 0
      src/main/java/org/example/service/TerminalService.java
  94. 149 0
      src/main/java/org/example/service/WardAreaService.java
  95. 41 0
      src/main/resources/application.properties
  96. 100 0
      src/main/resources/mapper/BedMapper.xml
  97. 58 0
      src/main/resources/mapper/DepartmentMapper.xml
  98. 32 0
      src/main/resources/mapper/HospitalMapper.xml
  99. 51 0
      src/main/resources/mapper/PatientChargeMapper.xml
  100. 51 0
      src/main/resources/mapper/PatientDiagnosisMapper.xml

+ 39 - 0
.gitignore

@@ -0,0 +1,39 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+.kotlin
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
+  </state>
+</component>

+ 7 - 0
.idea/encodings.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding" native2AsciiForPropertiesFiles="true" defaultCharsetForPropertiesFiles="UTF-8">
+    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
+  </component>
+</project>

+ 15 - 0
.idea/misc.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="KubernetesApiProvider"><![CDATA[{}]]></component>
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8 (3)" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 4 - 0
11.txt

@@ -0,0 +1,4 @@
+cd frontend
+npm install
+npm run dev
+reids缓存清除打开redis-cli,输入flushall

+ 24 - 0
frontend/.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 5 - 0
frontend/README.md

@@ -0,0 +1,5 @@
+# Vue 3 + Vite
+
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

+ 13 - 0
frontend/index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>智慧病房管理平台</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 1850 - 0
frontend/package-lock.json

@@ -0,0 +1,1850 @@
+{
+  "name": "frontend",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "frontend",
+      "version": "0.0.0",
+      "dependencies": {
+        "@wangeditor/editor": "^5.1.23",
+        "@wangeditor/editor-for-vue": "^5.1.12",
+        "axios": "^1.13.2",
+        "vue": "^3.5.24",
+        "vue-router": "^4.6.4"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^5.2.4",
+        "vite": "^5.4.21"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
+      "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+      "dependencies": {
+        "@babel/types": "^7.28.5"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
+      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
+      "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
+      "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
+      "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
+      "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
+      "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
+      "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
+      "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
+      "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
+      "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
+      "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
+      "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
+      "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
+      "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
+      "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
+      "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
+      "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
+      "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
+      "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
+      "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
+      "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
+      "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
+      "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
+      "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@transloadit/prettier-bytes": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
+      "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA=="
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true
+    },
+    "node_modules/@types/event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ=="
+    },
+    "node_modules/@uppy/companion-client": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz",
+      "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
+      "dependencies": {
+        "@uppy/utils": "^4.1.2",
+        "namespace-emitter": "^2.0.1"
+      }
+    },
+    "node_modules/@uppy/core": {
+      "version": "2.3.4",
+      "resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
+      "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
+      "dependencies": {
+        "@transloadit/prettier-bytes": "0.0.7",
+        "@uppy/store-default": "^2.1.1",
+        "@uppy/utils": "^4.1.3",
+        "lodash.throttle": "^4.1.1",
+        "mime-match": "^1.0.2",
+        "namespace-emitter": "^2.0.1",
+        "nanoid": "^3.1.25",
+        "preact": "^10.5.13"
+      }
+    },
+    "node_modules/@uppy/store-default": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz",
+      "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ=="
+    },
+    "node_modules/@uppy/utils": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz",
+      "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
+      "dependencies": {
+        "lodash.throttle": "^4.1.1"
+      }
+    },
+    "node_modules/@uppy/xhr-upload": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
+      "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
+      "dependencies": {
+        "@uppy/companion-client": "^2.2.2",
+        "@uppy/utils": "^4.1.2",
+        "nanoid": "^3.1.25"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^2.3.3"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.2.4",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+      "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+      "dev": true,
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
+      "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@vue/shared": "3.5.26",
+        "entities": "^7.0.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
+      "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
+      "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@vue/compiler-core": "3.5.26",
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/compiler-ssr": "3.5.26",
+        "@vue/shared": "3.5.26",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
+      "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.26.tgz",
+      "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
+      "dependencies": {
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.26.tgz",
+      "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz",
+      "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
+      "dependencies": {
+        "@vue/reactivity": "3.5.26",
+        "@vue/runtime-core": "3.5.26",
+        "@vue/shared": "3.5.26",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.26.tgz",
+      "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.26",
+        "@vue/shared": "3.5.26"
+      },
+      "peerDependencies": {
+        "vue": "3.5.26"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.26.tgz",
+      "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A=="
+    },
+    "node_modules/@wangeditor/basic-modules": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
+      "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
+      "dependencies": {
+        "is-url": "^1.2.4"
+      },
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.throttle": "^4.1.1",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/code-highlight": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
+      "integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
+      "dependencies": {
+        "prismjs": "^1.23.0"
+      },
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/core": {
+      "version": "1.1.19",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
+      "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
+      "dependencies": {
+        "@types/event-emitter": "^0.3.3",
+        "event-emitter": "^0.3.5",
+        "html-void-elements": "^2.0.0",
+        "i18next": "^20.4.0",
+        "scroll-into-view-if-needed": "^2.2.28",
+        "slate-history": "^0.66.0"
+      },
+      "peerDependencies": {
+        "@uppy/core": "^2.1.1",
+        "@uppy/xhr-upload": "^2.0.3",
+        "dom7": "^3.0.0",
+        "is-hotkey": "^0.2.0",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "lodash.foreach": "^4.5.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "lodash.toarray": "^4.4.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/editor": {
+      "version": "5.1.23",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
+      "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
+      "dependencies": {
+        "@uppy/core": "^2.1.1",
+        "@uppy/xhr-upload": "^2.0.3",
+        "@wangeditor/basic-modules": "^1.1.7",
+        "@wangeditor/code-highlight": "^1.0.3",
+        "@wangeditor/core": "^1.1.19",
+        "@wangeditor/list-module": "^1.0.5",
+        "@wangeditor/table-module": "^1.1.4",
+        "@wangeditor/upload-image-module": "^1.0.2",
+        "@wangeditor/video-module": "^1.1.4",
+        "dom7": "^3.0.0",
+        "is-hotkey": "^0.2.0",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "lodash.foreach": "^4.5.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "lodash.toarray": "^4.4.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/editor-for-vue": {
+      "version": "5.1.12",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
+      "integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
+      "peerDependencies": {
+        "@wangeditor/editor": ">=5.1.0",
+        "vue": "^3.0.5"
+      }
+    },
+    "node_modules/@wangeditor/list-module": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz",
+      "integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/table-module": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz",
+      "integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
+      "peerDependencies": {
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/upload-image-module": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
+      "integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
+      "peerDependencies": {
+        "@uppy/core": "^2.0.3",
+        "@uppy/xhr-upload": "^2.0.3",
+        "@wangeditor/basic-modules": "1.x",
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "lodash.foreach": "^4.5.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/@wangeditor/video-module": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz",
+      "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
+      "peerDependencies": {
+        "@uppy/core": "^2.1.4",
+        "@uppy/xhr-upload": "^2.0.7",
+        "@wangeditor/core": "1.x",
+        "dom7": "^3.0.0",
+        "nanoid": "^3.2.0",
+        "slate": "^0.72.0",
+        "snabbdom": "^3.1.0"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/axios": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz",
+      "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
+    },
+    "node_modules/d": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz",
+      "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+      "dependencies": {
+        "es5-ext": "^0.10.64",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dom7": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
+      "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
+      "dependencies": {
+        "ssr-window": "^3.0.0-alpha.1"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.0.tgz",
+      "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es5-ext": {
+      "version": "0.10.64",
+      "resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz",
+      "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+      "hasInstallScript": true,
+      "dependencies": {
+        "es6-iterator": "^2.0.3",
+        "es6-symbol": "^3.1.3",
+        "esniff": "^2.0.1",
+        "next-tick": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "node_modules/es6-symbol": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
+      "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+      "dependencies": {
+        "d": "^1.0.2",
+        "ext": "^1.7.0"
+      },
+      "engines": {
+        "node": ">=0.12"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/esniff": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz",
+      "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+      "dependencies": {
+        "d": "^1.0.1",
+        "es5-ext": "^0.10.62",
+        "event-emitter": "^0.3.5",
+        "type": "^2.7.2"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
+    "node_modules/event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+      "dependencies": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
+    "node_modules/ext": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz",
+      "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+      "dependencies": {
+        "type": "^2.7.2"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/html-void-elements": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz",
+      "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
+    "node_modules/i18next": {
+      "version": "20.6.1",
+      "resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz",
+      "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.0"
+      }
+    },
+    "node_modules/immer": {
+      "version": "9.0.21",
+      "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
+      "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
+    "node_modules/is-hotkey": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
+      "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
+    },
+    "node_modules/is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-url": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz",
+      "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
+    },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
+    },
+    "node_modules/lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+    },
+    "node_modules/lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
+    },
+    "node_modules/lodash.foreach": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
+      "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="
+    },
+    "node_modules/lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+      "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
+    },
+    "node_modules/lodash.throttle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+      "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
+    },
+    "node_modules/lodash.toarray": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+      "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw=="
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz",
+      "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
+      "dependencies": {
+        "wildcard": "^1.1.0"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/namespace-emitter": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
+      "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g=="
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/next-tick": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
+      "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/preact": {
+      "version": "10.28.1",
+      "resolved": "https://registry.npmmirror.com/preact/-/preact-10.28.1.tgz",
+      "integrity": "sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/preact"
+      }
+    },
+    "node_modules/prismjs": {
+      "version": "1.30.0",
+      "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz",
+      "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
+    "node_modules/rollup": {
+      "version": "4.54.0",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.54.0.tgz",
+      "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.54.0",
+        "@rollup/rollup-android-arm64": "4.54.0",
+        "@rollup/rollup-darwin-arm64": "4.54.0",
+        "@rollup/rollup-darwin-x64": "4.54.0",
+        "@rollup/rollup-freebsd-arm64": "4.54.0",
+        "@rollup/rollup-freebsd-x64": "4.54.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.54.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.54.0",
+        "@rollup/rollup-linux-arm64-musl": "4.54.0",
+        "@rollup/rollup-linux-loong64-gnu": "4.54.0",
+        "@rollup/rollup-linux-ppc64-gnu": "4.54.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.54.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.54.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.54.0",
+        "@rollup/rollup-linux-x64-gnu": "4.54.0",
+        "@rollup/rollup-linux-x64-musl": "4.54.0",
+        "@rollup/rollup-openharmony-arm64": "4.54.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.54.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.54.0",
+        "@rollup/rollup-win32-x64-gnu": "4.54.0",
+        "@rollup/rollup-win32-x64-msvc": "4.54.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
+      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
+    "node_modules/slate": {
+      "version": "0.72.8",
+      "resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
+      "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
+      "dependencies": {
+        "immer": "^9.0.6",
+        "is-plain-object": "^5.0.0",
+        "tiny-warning": "^1.0.3"
+      }
+    },
+    "node_modules/slate-history": {
+      "version": "0.66.0",
+      "resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz",
+      "integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
+      "dependencies": {
+        "is-plain-object": "^5.0.0"
+      },
+      "peerDependencies": {
+        "slate": ">=0.65.3"
+      }
+    },
+    "node_modules/snabbdom": {
+      "version": "3.6.3",
+      "resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.3.tgz",
+      "integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==",
+      "engines": {
+        "node": ">=12.17.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ssr-window": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz",
+      "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA=="
+    },
+    "node_modules/tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+    },
+    "node_modules/type": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz",
+      "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
+    },
+    "node_modules/vite": {
+      "version": "5.4.21",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+      "dev": true,
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz",
+      "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/compiler-sfc": "3.5.26",
+        "@vue/runtime-dom": "3.5.26",
+        "@vue/server-renderer": "3.5.26",
+        "@vue/shared": "3.5.26"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/wildcard": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz",
+      "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng=="
+    }
+  }
+}

+ 22 - 0
frontend/package.json

@@ -0,0 +1,22 @@
+{
+  "name": "frontend",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^5.1.12",
+    "axios": "^1.13.2",
+    "vue": "^3.5.24",
+    "vue-router": "^4.6.4"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.2.4",
+    "vite": "^5.4.21"
+  }
+}

BIN
frontend/public/img/52a0a789-b7af-4b73-bc8e-85ca74f80965.jpg


BIN
frontend/public/img/b223d09a-5649-4793-b316-7473bb78eb90.jpg


+ 1 - 0
frontend/public/vite.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 9 - 0
frontend/src/App.vue

@@ -0,0 +1,9 @@
+<script setup>
+</script>
+
+<template>
+  <router-view />
+</template>
+
+<style scoped>
+</style>

+ 1 - 0
frontend/src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 43 - 0
frontend/src/components/HelloWorld.vue

@@ -0,0 +1,43 @@
+<script setup>
+import { ref } from 'vue'
+
+defineProps({
+  msg: String,
+})
+
+const count = ref(0)
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+
+  <div class="card">
+    <button type="button" @click="count++">count is {{ count }}</button>
+    <p>
+      Edit
+      <code>components/HelloWorld.vue</code> to test HMR
+    </p>
+  </div>
+
+  <p>
+    Check out
+    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
+      >create-vue</a
+    >, the official Vue + Vite starter
+  </p>
+  <p>
+    Learn more about IDE Support for Vue in the
+    <a
+      href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
+      target="_blank"
+      >Vue Docs Scaling up Guide</a
+    >.
+  </p>
+  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
+</template>
+
+<style scoped>
+.read-the-docs {
+  color: #888;
+}
+</style>

+ 8 - 0
frontend/src/main.js

@@ -0,0 +1,8 @@
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+app.use(router)
+app.mount('#app')

+ 69 - 0
frontend/src/router/index.js

@@ -0,0 +1,69 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const routes = [
+  {
+    path: '/',
+    redirect: '/login'
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/Login.vue')
+  },
+  {
+    path: '/',
+    component: () => import('@/views/Layout.vue'),
+    children: [
+      {
+        path: 'home',
+        name: 'Home',
+        redirect: '/hospital/overview'
+      },
+      // 院区信息管理
+      {
+        path: 'hospital/overview',
+        name: 'HospitalOverview',
+        component: () => import('@/views/hospital/Overview.vue')
+      },
+      {
+        path: 'hospital/department',
+        name: 'Department',
+        component: () => import('@/views/hospital/Department.vue')
+      },
+      {
+        path: 'hospital/ward-area',
+        name: 'WardArea',
+        component: () => import('@/views/hospital/WardArea.vue')
+      },
+      {
+        path: 'hospital/ward',
+        name: 'Ward',
+        component: () => import('@/views/hospital/Room.vue')
+      },
+      {
+        path: 'hospital/bed',
+        name: 'Bed',
+        component: () => import('@/views/hospital/Bed.vue')
+      },
+      // 患者管理
+      {
+        path: 'hospital/patient',
+        name: 'Patient',
+        component: () => import('@/views/patient/Index.vue')
+      },
+      // 设备管理
+      {
+        path: 'device',
+        name: 'Device',
+        component: () => import('@/views/device/Index.vue')
+      }
+    ]
+  }
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+})
+
+export default router

+ 38 - 0
frontend/src/style.css

@@ -0,0 +1,38 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+:root {
+  font-family: 'Microsoft YaHei', system-ui, Avenir, Helvetica, Arial, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+  color: #333;
+  background-color: #f5f5f5;
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+  margin: 0;
+  min-width: 320px;
+  min-height: 100vh;
+}
+
+a {
+  font-weight: 500;
+  color: #667eea;
+  text-decoration: none;
+}
+
+a:hover {
+  color: #764ba2;
+}
+
+#app {
+  width: 100%;
+  min-height: 100vh;
+}

+ 40 - 0
frontend/src/utils/request.js

@@ -0,0 +1,40 @@
+import axios from 'axios'
+
+// 创建axios实例
+const request = axios.create({
+  baseURL: '/shixian',
+  timeout: 10000
+})
+
+// 请求拦截器
+request.interceptors.request.use(
+  config => {
+    // 可以在这里添加token等请求头
+    const token = localStorage.getItem('token')
+    if (token) {
+      config.headers['Authorization'] = token
+    }
+    // 添加用户类型到请求头
+    const userType = localStorage.getItem('userType')
+    if (userType) {
+      config.headers['X-User-Type'] = userType
+    }
+    return config
+  },
+  error => {
+    return Promise.reject(error)
+  }
+)
+
+// 响应拦截器
+request.interceptors.response.use(
+  response => {
+    return response.data
+  },
+  error => {
+    console.error('请求错误:', error)
+    return Promise.reject(error)
+  }
+)
+
+export default request

+ 230 - 0
frontend/src/views/Layout.vue

@@ -0,0 +1,230 @@
+<template>
+  <div class="layout">
+    <!-- 顶部信息栏 -->
+    <div class="header">
+      <div class="header-left">
+        <span class="system-icon">🏥</span>
+        <span class="system-name">智慧病房管理平台</span>
+      </div>
+      <div class="header-right">
+        <span class="user-info">欢迎,{{ userInfo.name || userInfo.username }}</span>
+        <button class="logout-btn" @click="handleLogout">退出登录</button>
+      </div>
+    </div>
+
+    <div class="main-container">
+      <!-- 左侧导航栏 -->
+      <div class="sidebar">
+        <div class="menu">
+          <!-- 院区信息管理 -->
+          <div class="menu-item">
+            <div class="menu-title" @click="toggleMenu('hospital')">
+              <span class="menu-icon">🏢</span>
+              <span>院区信息管理</span>
+              <span class="arrow" :class="{ expanded: expandedMenus.hospital }">▶</span>
+            </div>
+            <div class="sub-menu" v-show="expandedMenus.hospital">
+              <router-link to="/hospital/overview" class="sub-menu-item">医院概况</router-link>
+              <router-link to="/hospital/department" class="sub-menu-item">科室管理</router-link>
+              <router-link to="/hospital/ward-area" class="sub-menu-item">病区管理</router-link>
+              <router-link to="/hospital/ward" class="sub-menu-item">病房管理</router-link>
+              <router-link to="/hospital/bed" class="sub-menu-item">床位管理</router-link>
+              <router-link to="/hospital/patient" class="sub-menu-item">患者管理</router-link>
+            </div>
+          </div>
+
+          <!-- 设备管理 -->
+          <div class="menu-item">
+            <router-link to="/device" class="menu-title menu-link">
+              <span class="menu-icon">🔧</span>
+              <span>设备管理</span>
+            </router-link>
+          </div>
+        </div>
+      </div>
+
+      <!-- 右侧内容区域 -->
+      <div class="content">
+        <router-view />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { reactive, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+const userInfo = reactive({
+  name: '',
+  username: ''
+})
+
+const expandedMenus = reactive({
+  hospital: true
+})
+
+onMounted(() => {
+  const info = localStorage.getItem('userInfo')
+  if (info) {
+    Object.assign(userInfo, JSON.parse(info))
+  }
+})
+
+const toggleMenu = (menu) => {
+  expandedMenus[menu] = !expandedMenus[menu]
+}
+
+const handleLogout = () => {
+  localStorage.removeItem('userInfo')
+  localStorage.removeItem('userType')
+  router.push('/login')
+}
+</script>
+
+<style scoped>
+.layout {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 顶部信息栏 */
+.header {
+  height: 60px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+  color: #fff;
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+}
+
+.system-icon {
+  font-size: 28px;
+  margin-right: 10px;
+}
+
+.system-name {
+  font-size: 20px;
+  font-weight: bold;
+}
+
+.header-right {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+}
+
+.user-info {
+  font-size: 14px;
+}
+
+.logout-btn {
+  padding: 6px 16px;
+  background: rgba(255, 255, 255, 0.2);
+  border: 1px solid rgba(255, 255, 255, 0.5);
+  color: #fff;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.logout-btn:hover {
+  background: rgba(255, 255, 255, 0.3);
+}
+
+/* 主容器 */
+.main-container {
+  flex: 1;
+  display: flex;
+}
+
+/* 左侧导航栏 */
+.sidebar {
+  width: 220px;
+  background: #304156;
+  min-height: calc(100vh - 60px);
+}
+
+.menu {
+  padding: 10px 0;
+}
+
+.menu-item {
+  color: #bfcbd9;
+}
+
+.menu-title {
+  display: flex;
+  align-items: center;
+  padding: 14px 20px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.menu-title:hover {
+  background: #263445;
+}
+
+.menu-link {
+  text-decoration: none;
+  color: #bfcbd9;
+}
+
+.menu-link.router-link-active {
+  background: #667eea;
+  color: #fff;
+}
+
+.menu-icon {
+  margin-right: 10px;
+  font-size: 18px;
+}
+
+.arrow {
+  margin-left: auto;
+  font-size: 12px;
+  transition: transform 0.3s;
+}
+
+.arrow.expanded {
+  transform: rotate(90deg);
+}
+
+.sub-menu {
+  background: #1f2d3d;
+}
+
+.sub-menu-item {
+  display: block;
+  padding: 12px 20px 12px 50px;
+  color: #bfcbd9;
+  text-decoration: none;
+  transition: all 0.3s;
+}
+
+.sub-menu-item:hover {
+  background: #001528;
+}
+
+.sub-menu-item.router-link-active {
+  background: #667eea;
+  color: #fff;
+}
+
+/* 右侧内容区域 */
+.content {
+  flex: 1;
+  padding: 20px;
+  background: #f0f2f5;
+  overflow-y: auto;
+}
+</style>

+ 253 - 0
frontend/src/views/Login.vue

@@ -0,0 +1,253 @@
+<template>
+  <div class="login-page">
+    <!-- 系统顶端信息栏 -->
+    <div class="header">
+      <div class="header-content">
+        <span class="system-icon">🏥</span>
+        <span class="system-name">智慧病房管理平台</span>
+      </div>
+    </div>
+
+    <!-- 登录区域 -->
+    <div class="login-wrapper">
+      <div class="login-container">
+        <h2 class="login-title">智慧病房管理平台</h2>
+        
+        <!-- 用户类型切换 -->
+        <div class="login-tabs">
+          <div 
+            class="tab-item" 
+            :class="{ active: loginType === 'user' }"
+            @click="loginType = 'user'"
+          >
+            <span class="tab-icon">👤</span>
+            用户登录
+          </div>
+          <div 
+            class="tab-item" 
+            :class="{ active: loginType === 'admin' }"
+            @click="loginType = 'admin'"
+          >
+            <span class="tab-icon">👨‍💼</span>
+            管理员登录
+          </div>
+        </div>
+
+        <!-- 登录表单 -->
+        <form @submit.prevent="handleLogin">
+          <div class="form-item">
+            <label>{{ loginType === 'admin' ? '管理员账号' : '用户名' }}</label>
+            <input 
+              v-model="form.username" 
+              type="text" 
+              :placeholder="loginType === 'admin' ? '请输入管理员账号' : '请输入用户名'" 
+            />
+          </div>
+          <div class="form-item">
+            <label>密码</label>
+            <input 
+              v-model="form.password" 
+              type="password" 
+              placeholder="请输入密码" 
+            />
+          </div>
+          <button type="submit" class="login-btn">登 录</button>
+        </form>
+      </div>
+    </div>
+
+    <!-- 底部版权信息 -->
+    <div class="footer">
+      <p>© 2025 智慧病房管理平台 All Rights Reserved</p>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import request from '@/utils/request'
+
+const router = useRouter()
+
+// 登录类型:user-用户登录,admin-管理员登录
+const loginType = ref('user')
+const loading = ref(false)
+
+const form = reactive({
+  username: '',
+  password: ''
+})
+
+const handleLogin = async () => {
+  if (!form.username || !form.password) {
+    alert('请输入用户名和密码')
+    return
+  }
+  
+  loading.value = true
+  try {
+    // 根据登录类型调用不同接口
+    const url = loginType.value === 'admin' ? '/api/login/admin' : '/api/login/user'
+    const res = await request.post(url, {
+      username: form.username,
+      password: form.password
+    })
+    
+    if (res.code === 200) {
+      // 保存登录信息
+      localStorage.setItem('userInfo', JSON.stringify(res.data))
+      localStorage.setItem('userType', res.userType)
+      // 直接跳转到首页
+      router.push('/home')
+    } else {
+      alert(res.message || '登录失败')
+    }
+  } catch (error) {
+    console.error('登录失败:', error)
+    alert('登录失败,请检查网络连接')
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
+<style scoped>
+.login-page {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+/* 顶部信息栏 */
+.header {
+  background-color: rgba(255, 255, 255, 0.95);
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+  padding: 15px 30px;
+}
+
+.header-content {
+  display: flex;
+  align-items: center;
+}
+
+.system-icon {
+  font-size: 28px;
+  margin-right: 10px;
+}
+
+.system-name {
+  font-size: 22px;
+  font-weight: bold;
+  color: #333;
+}
+
+/* 登录区域 */
+.login-wrapper {
+  flex: 1;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px;
+}
+
+.login-container {
+  width: 420px;
+  background: #fff;
+  border-radius: 12px;
+  padding: 40px;
+  box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
+}
+
+.login-title {
+  text-align: center;
+  color: #333;
+  font-size: 24px;
+  margin-bottom: 30px;
+}
+
+/* 登录类型切换 */
+.login-tabs {
+  display: flex;
+  margin-bottom: 30px;
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.tab-item {
+  flex: 1;
+  padding: 12px;
+  text-align: center;
+  cursor: pointer;
+  background: #f5f5f5;
+  color: #666;
+  transition: all 0.3s;
+  font-size: 15px;
+}
+
+.tab-item:first-child {
+  border-right: 1px solid #e0e0e0;
+}
+
+.tab-item.active {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+}
+
+.tab-icon {
+  margin-right: 6px;
+}
+
+/* 表单 */
+.form-item {
+  margin-bottom: 22px;
+}
+
+.form-item label {
+  display: block;
+  margin-bottom: 8px;
+  color: #333;
+  font-size: 14px;
+}
+
+.form-item input {
+  width: 100%;
+  padding: 12px 15px;
+  border: 1px solid #ddd;
+  border-radius: 6px;
+  box-sizing: border-box;
+  font-size: 14px;
+  transition: border-color 0.3s;
+}
+
+.form-item input:focus {
+  outline: none;
+  border-color: #667eea;
+}
+
+.login-btn {
+  width: 100%;
+  padding: 14px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 6px;
+  font-size: 16px;
+  cursor: pointer;
+  transition: opacity 0.3s;
+}
+
+.login-btn:hover {
+  opacity: 0.9;
+}
+
+/* 底部 */
+.footer {
+  text-align: center;
+  padding: 20px;
+  color: rgba(255, 255, 255, 0.8);
+  font-size: 13px;
+}
+</style>

+ 1007 - 0
frontend/src/views/device/Index.vue

@@ -0,0 +1,1007 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <span class="breadcrumb-label">当前位置:</span>
+      <span class="breadcrumb-current">设备管理</span>
+    </div>
+
+    <!-- 查询栏 -->
+    <div class="search-bar">
+      <div class="search-item">
+        <label>设备类型:</label>
+        <select v-model="searchForm.terminalType">
+          <option value="">全部</option>
+          <option value="C">床头分机</option>
+          <option value="D">病房门口机</option>
+          <option value="H">护士站主机</option>
+          <option value="X">护士站信息屏</option>
+          <option value="T">LCD走廊显示屏</option>
+          <option value="P">LED走廊显示屏</option>
+          <option value="M">病区门口机</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>所属科室:</label>
+        <select v-model="searchForm.deptCode" @change="onDeptChange">
+          <option value="">全部</option>
+          <option v-for="(dept, index) in deptList" :key="index" :value="dept">{{ dept }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>所属病区:</label>
+        <select v-model="searchForm.wardCode">
+          <option value="">全部</option>
+          <option v-for="(ward, index) in wardList" :key="index" :value="ward">{{ ward }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>在线状态:</label>
+        <select v-model="searchForm.isOnline">
+          <option value="">全部</option>
+          <option :value="1">在线</option>
+          <option :value="0">离线</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>设备名称:</label>
+        <input type="text" v-model="searchForm.terminalDesc" placeholder="请输入设备名称" />
+      </div>
+      <button class="search-btn" @click="handleSearch">查询</button>
+      <button class="reset-btn" @click="handleReset">重置</button>
+      <button class="add-btn" @click="handleAdd" v-if="isAdmin">新增</button>
+    </div>
+
+    <!-- 数据列表 -->
+    <div class="table-container">
+      <div v-if="loading" class="loading-mask">
+        <div class="loading-spinner"></div>
+        <span>加载中...</span>
+      </div>
+      <table class="data-table" v-show="!loading">
+        <thead>
+          <tr>
+            <th>设备类型</th>
+            <th>设备编码</th>
+            <th>设备名称</th>
+            <th>所属科室</th>
+            <th>所属病区</th>
+            <th>IP地址</th>
+            <th>子网掩码</th>
+            <th>默认网关</th>
+            <th>Mac地址</th>
+            <th>设备位置</th>
+            <th>在线状态</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="item in tableData" :key="item.id">
+            <td>{{ formatTerminalType(item.terminalType) }}</td>
+            <td>{{ item.terminalNumber }}</td>
+            <td>{{ item.terminalDesc }}</td>
+            <td>{{ item.deptCode }}</td>
+            <td>{{ item.wardCode }}</td>
+            <td>{{ item.ipAddress }}</td>
+            <td>{{ item.subnetMask }}</td>
+            <td>{{ item.gatewayAddress }}</td>
+            <td>{{ item.macAddress }}</td>
+            <td>{{ item.tenementName }}</td>
+            <td>
+              <span :class="item.isOnline === 1 ? 'status-online' : 'status-offline'">
+                {{ item.isOnline === 1 ? '在线' : '离线' }}
+              </span>
+            </td>
+          </tr>
+          <tr v-if="tableData.length === 0">
+            <td colspan="11" class="empty-tip">暂无数据</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination" v-if="total > 0">
+      <span class="pagination-info">共 {{ total }} 条记录</span>
+      <div class="pagination-controls">
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(1)">首页</button>
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(currentPage - 1)">上一页</button>
+        <span class="page-numbers">
+          <button 
+            v-for="page in displayPages" 
+            :key="page" 
+            class="page-num" 
+            :class="{ active: page === currentPage }"
+            @click="changePage(page)"
+          >{{ page }}</button>
+        </span>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(currentPage + 1)">下一页</button>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(totalPages)">末页</button>
+        <span class="page-size-selector">
+          <select v-model="pageSize" @change="handlePageSizeChange">
+            <option :value="10">10条/页</option>
+            <option :value="20">20条/页</option>
+            <option :value="50">50条/页</option>
+            <option :value="100">100条/页</option>
+          </select>
+        </span>
+        <span class="page-jump">
+          跳至 <input type="number" v-model.number="jumpPage" min="1" :max="totalPages" @keyup.enter="handleJumpPage" /> 页
+        </span>
+      </div>
+    </div>
+    
+    <!-- 新增设备弹窗 -->
+    <div v-if="showAddDialog" class="dialog-overlay" @click.self="closeAddDialog">
+      <div class="dialog-content">
+        <div class="dialog-header">
+          <span>新增设备</span>
+          <button class="close-btn" @click="closeAddDialog">&times;</button>
+        </div>
+        <div class="dialog-body">
+          <div class="form-row">
+            <label class="required">设备类型:</label>
+            <select v-model="addForm.terminalType" @change="onTypeChange">
+              <option value="">请选择</option>
+              <option value="C">床头分机</option>
+              <option value="D">病房门口机</option>
+              <option value="H">护士站主机</option>
+              <option value="X">护士站信息屏</option>
+              <option value="T">LCD走廊显示屏</option>
+              <option value="P">LED走廊显示屏</option>
+              <option value="M">病区门口机</option>
+            </select>
+          </div>
+          <div class="form-row">
+            <label>设备编码:</label>
+            <input type="text" v-model="generatedCode" readonly class="readonly-input" placeholder="系统自动生成" />
+          </div>
+          <div class="form-row">
+            <label>所属科室:</label>
+            <select v-model="addForm.selectedDept" @change="onAddDeptChange">
+              <option value="">请选择</option>
+              <option v-for="dept in presetDeptList" :key="dept" :value="dept">{{ dept }}</option>
+            </select>
+          </div>
+          <div class="form-row">
+            <label>所属病区:</label>
+            <select v-model="addForm.selectedWard" @change="onWardChange">
+              <option value="">请选择</option>
+              <option v-for="ward in realWardList" :key="ward.id" :value="ward.code">{{ ward.name }}</option>
+            </select>
+          </div>
+          
+          <!-- 设备名称组成部分 -->
+          <div class="form-section">
+            <div class="section-title">设备名称组成</div>
+            <div class="form-row">
+              <label>设备类型编号:</label>
+              <input type="text" :value="addForm.terminalType" readonly class="readonly-input short-input" />
+            </div>
+            <div class="form-row">
+              <label>病区代码:</label>
+              <input type="text" v-model="addForm.wardCode" readonly class="readonly-input short-input" placeholder="选择病区后自动带出" />
+            </div>
+            <div class="form-row" v-if="needRoomCode">
+              <label>病房代码:</label>
+              <input type="text" v-model="addForm.roomCode" maxlength="4" @blur="formatRoomCode" placeholder="4位数字" class="short-input" />
+            </div>
+            <div class="form-row" v-if="needBedNo">
+              <label>床位号:</label>
+              <input type="text" v-model="addForm.bedNo" maxlength="3" @blur="formatBedNo" placeholder="3位数字" class="short-input" />
+            </div>
+            <div class="form-row" v-if="needDeviceNo">
+              <label>设备号:</label>
+              <input type="text" v-model="addForm.deviceNo" maxlength="2" @blur="formatDeviceNo" placeholder="2位数字" class="short-input" />
+            </div>
+          </div>
+          
+          <div class="form-row">
+            <label class="required">IP地址:</label>
+            <input type="text" v-model="addForm.ipAddress" placeholder="如:192.168.1.100" />
+          </div>
+          <div class="form-row">
+            <label>子网掩码:</label>
+            <input type="text" v-model="addForm.subnetMask" placeholder="如:255.255.255.0" />
+          </div>
+          <div class="form-row">
+            <label>默认网关:</label>
+            <input type="text" v-model="addForm.gatewayAddress" placeholder="如:192.168.1.1" />
+          </div>
+          <div class="form-row">
+            <label>设备位置:</label>
+            <input type="text" v-model="addForm.tenementName" placeholder="请输入设备安装位置" />
+          </div>
+        </div>
+        <div class="dialog-footer">
+          <button class="cancel-btn" @click="closeAddDialog">取消</button>
+          <button class="confirm-btn" @click="submitAdd">确定</button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from 'vue'
+import request from '@/utils/request'
+
+// 判断是否为管理员
+const isAdmin = computed(() => {
+  const userType = localStorage.getItem('userType')
+  return userType === 'admin'
+})
+
+// 查询条件
+const searchForm = reactive({
+  terminalType: '',
+  deptCode: '',
+  wardCode: '',
+  isOnline: '',
+  terminalDesc: ''
+})
+
+// 下拉列表数据
+const deptList = ref([])
+const wardList = ref([])
+
+// 表格数据
+const tableData = ref([])
+
+// 加载状态
+const loading = ref(false)
+
+// 分页相关
+const currentPage = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const jumpPage = ref(1)
+
+// 计算总页数
+const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1)
+
+// 计算显示的页码
+const displayPages = computed(() => {
+  const pages = []
+  const maxShow = 5
+  let start = Math.max(1, currentPage.value - Math.floor(maxShow / 2))
+  let end = Math.min(totalPages.value, start + maxShow - 1)
+  if (end - start + 1 < maxShow) {
+    start = Math.max(1, end - maxShow + 1)
+  }
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  return pages
+})
+
+// 新增弹窗
+const showAddDialog = ref(false)
+const realDeptList = ref([])
+const realWardList = ref([])
+
+// 新增表单
+const addForm = reactive({
+  terminalType: '',
+  selectedDept: '',
+  selectedWard: '',
+  wardCode: '',
+  roomCode: '',
+  bedNo: '',
+  deviceNo: '',
+  ipAddress: '',
+  subnetMask: '255.255.255.0',
+  gatewayAddress: '',
+  tenementName: ''
+})
+
+// 设备类型映射
+const terminalTypeMap = {
+  'C': '床头分机',
+  'D': '病房门口机',
+  'H': '护士站主机',
+  'X': '护士站信息屏',
+  'T': 'LCD走廊显示屏',
+  'P': 'LED走廊显示屏',
+  'M': '病区门口机'
+}
+
+// 预设的科室和病区数据
+const deptWardData = {
+  '急诊科': [
+    { code: '01', name: '抢救区' },
+    { code: '02', name: '危重诊疗区' },
+    { code: '03', name: '普通诊疗区' },
+    { code: '04', name: '急诊观察室' },
+    { code: '05', name: '急诊综合病房/EICU' }
+  ],
+  '综合内科': [
+    { code: '01', name: '综合内科一病区' },
+    { code: '02', name: '综合内科二病区' },
+    { code: '03', name: '综合内科三病区' }
+  ],
+  '康复医学科': [
+    { code: '01', name: '神经康复病区' },
+    { code: '02', name: '骨关节康复病区' },
+    { code: '03', name: '心肺康复病区' }
+  ],
+  '心血管内科': [
+    { code: '01', name: '冠心病病区' },
+    { code: '02', name: '心律失常病区' },
+    { code: '03', name: '心力衰竭与重症病区' },
+    { code: '04', name: '高血压与心脏康复病区' },
+    { code: '05', name: '心脏监护病房(CCU)' }
+  ],
+  '内分泌科': [
+    { code: '01', name: '糖尿病病区' },
+    { code: '02', name: '甲状腺与代谢病病区' },
+    { code: '03', name: '肥胖与代谢综合征病区' }
+  ],
+  '神经内科': [
+    { code: '01', name: '脑血管病病区' },
+    { code: '02', name: '运动障碍与神经变性病病区' },
+    { code: '03', name: '癫痫与睡眠障碍病区' },
+    { code: '04', name: '神经免疫与感染病区' }
+  ],
+  '骨科': [
+    { code: '01', name: '脊柱外科病区' },
+    { code: '02', name: '关节外科病区' },
+    { code: '03', name: '创伤骨科病区' },
+    { code: '04', name: '手外科/显微外科病区' },
+    { code: '05', name: '运动医学科病区' }
+  ],
+  '眼科': [
+    { code: '01', name: '一病区' }
+  ],
+  '耳鼻喉科': [
+    { code: '01', name: '耳科与颅底外科病区' },
+    { code: '02', name: '鼻科与过敏性疾病病区' },
+    { code: '03', name: '咽喉头颈外科病区' }
+  ],
+  '口腔科': [
+    { code: '01', name: '口腔颌面外科病区' }
+  ],
+  '皮肤科': [
+    { code: '01', name: '普通皮肤科病区' },
+    { code: '02', name: '皮肤外科与美容病区' }
+  ],
+  '中医科': [
+    { code: '01', name: '针灸推拿病区' },
+    { code: '02', name: '康复病区' },
+    { code: '03', name: '中医内科病区' },
+    { code: '04', name: '中医骨伤病区' },
+    { code: '05', name: '纯中医治疗病区' },
+    { code: '06', name: '中西医结合病区' }
+  ],
+  '医学影像科': [
+    { code: '01', name: 'X线/DR检查区' },
+    { code: '02', name: 'CT检查区' },
+    { code: '03', name: '磁共振检查区' },
+    { code: '04', name: '超声检查区' },
+    { code: '05', name: '临检室' }
+  ]
+}
+
+// 获取科室列表
+const presetDeptList = computed(() => Object.keys(deptWardData))
+
+// 是否需要病房代码(床头分机、病房门口机)
+const needRoomCode = computed(() => ['C', 'D'].includes(addForm.terminalType))
+
+// 是否需要床位号(只有床头分机)
+const needBedNo = computed(() => addForm.terminalType === 'C')
+
+// 是否需要设备号(护士站主机、护士站信息屏、走廊显示屏、病区门口机)
+const needDeviceNo = computed(() => ['H', 'X', 'T', 'P', 'M'].includes(addForm.terminalType))
+
+// 生成的设备编码
+const generatedCode = computed(() => {
+  let code = addForm.terminalType || ''
+  if (addForm.wardCode) code += addForm.wardCode
+  if (needRoomCode.value && addForm.roomCode) code += addForm.roomCode
+  if (needBedNo.value && addForm.bedNo) code += addForm.bedNo
+  if (needDeviceNo.value && addForm.deviceNo) code += addForm.deviceNo
+  return code
+})
+
+onMounted(async () => {
+  loading.value = true
+  try {
+    // 使用合并接口,一次请求获取所有数据
+    const res = await request.get('/api/terminal/init-data', {
+      params: {
+        page: currentPage.value,
+        pageSize: pageSize.value
+      }
+    })
+    if (res.code === 200 && res.data) {
+      deptList.value = res.data.deptList || []
+      wardList.value = res.data.wardList || []
+      tableData.value = res.data.terminalList?.records || res.data.terminalList || []
+      total.value = res.data.terminalList?.total || tableData.value.length
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+})
+
+// 加载所属科室列表
+const loadDeptList = async () => {
+  try {
+    const res = await request.get('/api/terminal/dept-list')
+    if (res.code === 200) {
+      deptList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载科室列表失败:', error)
+  }
+}
+
+// 加载病区列表
+const loadWardList = async () => {
+  try {
+    const params = {}
+    if (searchForm.deptCode) params.deptCode = searchForm.deptCode
+    const res = await request.get('/api/terminal/ward-list', { params })
+    if (res.code === 200) {
+      wardList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载病区列表失败:', error)
+  }
+}
+
+// 科室变化时
+const onDeptChange = () => {
+  searchForm.wardCode = ''
+  loadWardList()
+}
+
+// 加载数据
+const loadData = async () => {
+  loading.value = true
+  try {
+    const params = {
+      page: currentPage.value,
+      pageSize: pageSize.value
+    }
+    if (searchForm.terminalType) params.terminalType = searchForm.terminalType
+    if (searchForm.deptCode) params.deptCode = searchForm.deptCode
+    if (searchForm.wardCode) params.wardCode = searchForm.wardCode
+    if (searchForm.isOnline !== '') params.isOnline = searchForm.isOnline
+    if (searchForm.terminalDesc) params.terminalDesc = searchForm.terminalDesc
+    
+    const res = await request.get('/api/terminal/list', { params })
+    if (res.code === 200) {
+      tableData.value = res.data?.records || res.data || []
+      total.value = res.data?.total || tableData.value.length
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 切换页码
+const changePage = (page) => {
+  if (page >= 1 && page <= totalPages.value) {
+    currentPage.value = page
+    jumpPage.value = page
+    loadData()
+  }
+}
+
+// 修改每页条数
+const handlePageSizeChange = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 跳转到指定页
+const handleJumpPage = () => {
+  const page = Math.min(Math.max(1, jumpPage.value), totalPages.value)
+  changePage(page)
+}
+
+// 查询
+const handleSearch = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 重置
+const handleReset = () => {
+  searchForm.terminalType = ''
+  searchForm.deptCode = ''
+  searchForm.wardCode = ''
+  searchForm.isOnline = ''
+  searchForm.terminalDesc = ''
+  currentPage.value = 1
+  loadWardList()
+  loadData()
+}
+
+// 新增按钮点击
+const handleAdd = () => {
+  // 重置表单
+  resetAddForm()
+  showAddDialog.value = true
+}
+
+// 重置新增表单
+const resetAddForm = () => {
+  addForm.terminalType = ''
+  addForm.selectedDept = ''
+  addForm.selectedWard = ''
+  addForm.wardCode = ''
+  addForm.roomCode = ''
+  addForm.bedNo = ''
+  addForm.deviceNo = ''
+  addForm.ipAddress = ''
+  addForm.subnetMask = '255.255.255.0'
+  addForm.gatewayAddress = ''
+  addForm.tenementName = ''
+  realWardList.value = []
+}
+
+// 关闭新增弹窗
+const closeAddDialog = () => {
+  showAddDialog.value = false
+}
+
+// 设备类型变化
+const onTypeChange = () => {
+  // 清空相关字段
+  addForm.roomCode = ''
+  addForm.bedNo = ''
+  addForm.deviceNo = ''
+}
+
+// 新增弹窗中科室变化
+const onAddDeptChange = () => {
+  addForm.selectedWard = ''
+  addForm.wardCode = ''
+  if (!addForm.selectedDept) {
+    realWardList.value = []
+    return
+  }
+  // 从预设数据中获取病区列表
+  realWardList.value = deptWardData[addForm.selectedDept] || []
+}
+
+// 病区变化
+const onWardChange = () => {
+  const selected = realWardList.value.find(w => w.code === addForm.selectedWard)
+  if (selected) {
+    addForm.wardCode = selected.code || ''
+  } else {
+    addForm.wardCode = ''
+  }
+}
+
+// 格式化病房代码(4位数字,不足前补0)
+const formatRoomCode = () => {
+  if (addForm.roomCode) {
+    const num = addForm.roomCode.replace(/\D/g, '')
+    addForm.roomCode = num.padStart(4, '0').slice(-4)
+  }
+}
+
+// 格式化床位号(3位数字,不足前补0)
+const formatBedNo = () => {
+  if (addForm.bedNo) {
+    const num = addForm.bedNo.replace(/\D/g, '')
+    addForm.bedNo = num.padStart(3, '0').slice(-3)
+  }
+}
+
+// 格式化设备号(2位数字,不足前补0)
+const formatDeviceNo = () => {
+  if (addForm.deviceNo) {
+    const num = addForm.deviceNo.replace(/\D/g, '')
+    addForm.deviceNo = num.padStart(2, '0').slice(-2)
+  }
+}
+
+// 提交新增
+const submitAdd = async () => {
+  // 校验必填字段
+  if (!addForm.terminalType) {
+    alert('请选择设备类型')
+    return
+  }
+  if (!addForm.ipAddress) {
+    alert('请输入IP地址')
+    return
+  }
+  
+  // 构造设备名称
+  let terminalDesc = ''
+  if (addForm.wardCode) terminalDesc += addForm.wardCode
+  if (needRoomCode.value && addForm.roomCode) terminalDesc += addForm.roomCode
+  if (needBedNo.value && addForm.bedNo) terminalDesc += addForm.bedNo
+  if (needDeviceNo.value && addForm.deviceNo) terminalDesc += addForm.deviceNo
+  
+  // 构造请求数据
+  const data = {
+    terminalType: addForm.terminalType,
+    terminalDesc: terminalDesc,
+    deptCode: addForm.selectedDept,
+    wardCode: addForm.selectedWard ? (realWardList.value.find(w => w.code === addForm.selectedWard)?.name || '') : '',
+    ipAddress: addForm.ipAddress,
+    subnetMask: addForm.subnetMask,
+    gatewayAddress: addForm.gatewayAddress,
+    tenementName: addForm.tenementName
+  }
+  
+  try {
+    const res = await request.post('/api/terminal/add', data)
+    if (res.code === 200) {
+      alert('新增成功')
+      closeAddDialog()
+      loadData()
+    } else {
+      alert(res.message || '新增失败')
+    }
+  } catch (error) {
+    console.error('新增失败:', error)
+    alert('新增失败')
+  }
+}
+
+// 格式化设备类型
+const formatTerminalType = (type) => {
+  const map = {
+    'S': '室内机',
+    'M': '门口机',
+    'R': '入口机',
+    'Z': '中心机',
+    'F': '室内分机',
+    'L': '数字门铃',
+    'H': '护士站主机',
+    'C': '床头分机',
+    'D': '病房门口机',
+    'X': '护士站信息屏',
+    'T': 'LCD走廊显示屏',
+    'P': 'LED走廊显示屏'
+  }
+  return map[type] || type || ''
+}
+</script>
+
+<style scoped>
+.page-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+}
+.page-header {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 15px;
+  margin-bottom: 20px;
+  font-size: 14px;
+}
+.breadcrumb-label { color: #666; }
+.breadcrumb-current { color: #409eff; }
+
+/* 查询栏 */
+.search-bar {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin-bottom: 20px;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  flex-wrap: wrap;
+}
+.search-item {
+  display: flex;
+  align-items: center;
+}
+.search-item label {
+  margin-right: 8px;
+  color: #666;
+  font-size: 14px;
+  white-space: nowrap;
+}
+.search-item input,
+.search-item select {
+  padding: 8px 12px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 14px;
+}
+.search-item input { width: 150px; }
+.search-item select { width: 150px; }
+.search-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+.search-btn:hover { background: #66b1ff; }
+.reset-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #666;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+.reset-btn:hover { border-color: #409eff; color: #409eff; }
+.add-btn {
+  padding: 8px 20px;
+  background: #67c23a;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+.add-btn:hover { background: #85ce61; }
+
+/* 数据表格 */
+.table-container {
+  overflow-x: auto;
+}
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 14px;
+}
+.data-table th,
+.data-table td {
+  padding: 12px;
+  text-align: left;
+  border-bottom: 1px solid #eee;
+  white-space: nowrap;
+}
+.data-table th {
+  background: #f5f7fa;
+  font-weight: 500;
+  color: #333;
+}
+.data-table tbody tr:hover {
+  background: #f5f7fa;
+}
+.empty-tip {
+  text-align: center;
+  color: #999;
+  padding: 40px 0;
+}
+
+/* 加载状态 */
+.loading-mask {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 0;
+  color: #909399;
+}
+.loading-spinner {
+  width: 32px;
+  height: 32px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 12px;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+/* 状态样式 */
+.status-online {
+  color: #67c23a;
+  font-weight: 500;
+}
+.status-offline {
+  color: #f56c6c;
+  font-weight: 500;
+}
+
+/* 弹窗样式 */
+.dialog-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+.dialog-content {
+  background: #fff;
+  border-radius: 8px;
+  width: 600px;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+.dialog-header {
+  padding: 15px 20px;
+  border-bottom: 1px solid #eee;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 16px;
+  font-weight: 500;
+}
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 24px;
+  cursor: pointer;
+  color: #999;
+}
+.close-btn:hover { color: #666; }
+.dialog-body {
+  padding: 20px;
+}
+.dialog-footer {
+  padding: 15px 20px;
+  border-top: 1px solid #eee;
+  text-align: right;
+}
+.form-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 15px;
+}
+.form-row label {
+  width: 120px;
+  text-align: right;
+  padding-right: 10px;
+  color: #666;
+  font-size: 14px;
+}
+.form-row label.required::before {
+  content: '*';
+  color: #f56c6c;
+  margin-right: 4px;
+}
+.form-row input,
+.form-row select {
+  flex: 1;
+  padding: 8px 12px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 14px;
+}
+.form-row input:focus,
+.form-row select:focus {
+  border-color: #409eff;
+  outline: none;
+}
+.readonly-input {
+  background: #f5f7fa;
+  cursor: not-allowed;
+}
+.short-input {
+  flex: none !important;
+  width: 120px;
+}
+.form-section {
+  background: #f9f9f9;
+  padding: 15px;
+  border-radius: 4px;
+  margin: 15px 0;
+}
+.section-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 15px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #eee;
+}
+.cancel-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #666;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  margin-right: 10px;
+}
+.cancel-btn:hover { border-color: #409eff; color: #409eff; }
+.confirm-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.confirm-btn:hover { background: #66b1ff; }
+
+/* 分页样式 */
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  padding: 15px 0;
+  border-top: 1px solid #eee;
+}
+.pagination-info {
+  color: #666;
+  font-size: 14px;
+}
+.pagination-controls {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.page-btn {
+  padding: 6px 12px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-btn:hover:not(:disabled) {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-btn:disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+.page-numbers {
+  display: flex;
+  gap: 5px;
+}
+.page-num {
+  min-width: 32px;
+  height: 28px;
+  padding: 0 6px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-num:hover {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-num.active {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+}
+.page-size-selector select {
+  padding: 5px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 13px;
+  margin-left: 10px;
+}
+.page-jump {
+  font-size: 13px;
+  color: #606266;
+  margin-left: 10px;
+}
+.page-jump input {
+  width: 50px;
+  padding: 5px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  text-align: center;
+  margin: 0 5px;
+}</style>

+ 820 - 0
frontend/src/views/hospital/Bed.vue

@@ -0,0 +1,820 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <span class="breadcrumb-label">当前位置:</span>
+      <span class="breadcrumb-path">院区信息管理</span>
+      <span class="breadcrumb-separator">&gt;</span>
+      <span class="breadcrumb-current">床位管理</span>
+    </div>
+
+    <!-- 查询栏 -->
+    <div class="search-bar">
+      <div class="search-item">
+        <label>床位代码:</label>
+        <input type="text" v-model="searchForm.code" placeholder="请输入床位代码" />
+      </div>
+      <div class="search-item">
+        <label>所属科室:</label>
+        <select v-model="searchForm.belongDept" @change="onDeptChange">
+          <option value="">全部</option>
+          <option v-for="(dept, index) in deptList" :key="index" :value="dept">{{ dept }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>所属病区:</label>
+        <select v-model="searchForm.belongWard" @change="onWardChange">
+          <option value="">全部</option>
+          <option v-for="(ward, index) in wardList" :key="index" :value="ward">{{ ward }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>所属病房:</label>
+        <select v-model="searchForm.belongRoom">
+          <option value="">全部</option>
+          <option v-for="(room, index) in roomList" :key="index" :value="room">{{ room }}</option>
+        </select>
+      </div>
+      <button class="search-btn" @click="handleSearch">查询</button>
+      <button class="reset-btn" @click="handleReset">重置</button>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="action-bar" v-if="isAdmin">
+      <button class="code-set-btn" @click="openCodeSetting" :disabled="selectedRows.length === 0">
+        床位代码设置
+      </button>
+    </div>
+
+    <!-- 数据列表 -->
+    <div class="table-container">
+      <div v-if="loading" class="loading-mask">
+        <div class="loading-spinner"></div>
+        <span>加载中...</span>
+      </div>
+      <table class="data-table" v-show="!loading">
+        <thead>
+          <tr>
+            <th><input type="checkbox" :checked="selectAll" @change="handleSelectAll" /></th>
+            <th>床位代码</th>
+            <th>床位外部代码</th>
+            <th>床位名称</th>
+            <th>床位类型</th>
+            <th>所属科室</th>
+            <th>所属病区</th>
+            <th>所属病房</th>
+            <th>使用状态</th>
+            <th>是否启用</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="item in tableData" :key="item.id">
+            <td><input type="checkbox" :checked="isSelected(item)" @change="toggleSelect(item)" /></td>
+            <td>{{ item.code }}</td>
+            <td>{{ item.outCode }}</td>
+            <td>{{ item.name }}</td>
+            <td>{{ item.bedType }}</td>
+            <td>{{ item.belongDept }}</td>
+            <td>{{ item.belongWard }}</td>
+            <td>{{ item.belongRoom }}</td>
+            <td>{{ item.used === 1 ? '已使用' : '未使用' }}</td>
+            <td>{{ item.enabled === 1 ? '是' : '否' }}</td>
+            <td>
+              <button class="detail-btn" @click="showDetail(item)">详情</button>
+            </td>
+          </tr>
+          <tr v-if="tableData.length === 0">
+            <td colspan="11" class="empty-tip">暂无数据</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination" v-if="total > 0">
+      <span class="pagination-info">共 {{ total }} 条记录</span>
+      <div class="pagination-controls">
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(1)">首页</button>
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(currentPage - 1)">上一页</button>
+        <span class="page-numbers">
+          <button 
+            v-for="page in displayPages" 
+            :key="page" 
+            class="page-num" 
+            :class="{ active: page === currentPage }"
+            @click="changePage(page)"
+          >{{ page }}</button>
+        </span>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(currentPage + 1)">下一页</button>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(totalPages)">末页</button>
+        <span class="page-size-selector">
+          <select v-model="pageSize" @change="handlePageSizeChange">
+            <option :value="10">10条/页</option>
+            <option :value="20">20条/页</option>
+            <option :value="50">50条/页</option>
+            <option :value="100">100条/页</option>
+          </select>
+        </span>
+        <span class="page-jump">
+          跳至 <input type="number" v-model.number="jumpPage" min="1" :max="totalPages" @keyup.enter="handleJumpPage" /> 页
+        </span>
+      </div>
+    </div>
+
+    <!-- 详情弹窗 -->
+    <div class="modal-overlay" v-if="detailVisible" @click.self="detailVisible = false">
+      <div class="modal-content">
+        <div class="modal-header">
+          <span>床位详情</span>
+          <button class="close-btn" @click="detailVisible = false">×</button>
+        </div>
+        <div class="modal-body">
+          <div class="detail-row"><span class="label">床位代码:</span><span class="value">{{ currentBed.code }}</span></div>
+          <div class="detail-row"><span class="label">床位外部代码:</span><span class="value">{{ currentBed.outCode }}</span></div>
+          <div class="detail-row"><span class="label">床位名称:</span><span class="value">{{ currentBed.name }}</span></div>
+          <div class="detail-row"><span class="label">床位类型:</span><span class="value">{{ currentBed.bedType }}</span></div>
+          <div class="detail-row"><span class="label">所属科室:</span><span class="value">{{ currentBed.belongDept }}</span></div>
+          <div class="detail-row"><span class="label">所属病区:</span><span class="value">{{ currentBed.belongWard }}</span></div>
+          <div class="detail-row"><span class="label">所属病房:</span><span class="value">{{ currentBed.belongRoom }}</span></div>
+          <div class="detail-row"><span class="label">管床医生:</span><span class="value">{{ currentBed.doctor }}</span></div>
+          <div class="detail-row"><span class="label">管床护士:</span><span class="value">{{ currentBed.nurse }}</span></div>
+          <div class="detail-row"><span class="label">使用状态:</span><span class="value">{{ currentBed.used === 1 ? '已使用' : '未使用' }}</span></div>
+          <div class="detail-row"><span class="label">排序号:</span><span class="value">{{ currentBed.sort }}</span></div>
+          <div class="detail-row"><span class="label">是否启用:</span><span class="value">{{ currentBed.enabled === 1 ? '是' : '否' }}</span></div>
+          <div class="detail-row"><span class="label">备注:</span><span class="value">{{ currentBed.remark }}</span></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 床位代码设置弹窗 -->
+    <div class="modal-overlay" v-if="codeSettingVisible" @click.self="codeSettingVisible = false">
+      <div class="modal-content code-setting-modal">
+        <div class="modal-header">
+          <span>床位代码设置</span>
+          <button class="close-btn" @click="codeSettingVisible = false">×</button>
+        </div>
+        <div class="modal-body">
+          <table class="code-setting-table">
+            <thead>
+              <tr>
+                <th>床位名称</th>
+                <th>所属病区</th>
+                <th>当前代码</th>
+                <th>新代码</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="(item, index) in codeSettingList" :key="item.id">
+                <td>{{ item.name }}</td>
+                <td>{{ item.belongWard }}</td>
+                <td>{{ item.code }}</td>
+                <td>
+                  <input type="text" v-model="item.newCode" maxlength="3" 
+                         placeholder="001~999" @blur="formatCodeInput(index)" />
+                </td>
+              </tr>
+            </tbody>
+          </table>
+          <div class="code-tips">
+            提示:床位代码为001~999三位数字,输入1我12将自动补0变为001与012,同病区下代码不可重复
+          </div>
+        </div>
+        <div class="modal-footer">
+          <button class="cancel-btn" @click="codeSettingVisible = false">取消</button>
+          <button class="confirm-btn" @click="saveCodeSetting">保存</button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import request from '@/utils/request'
+
+// 判断是否为管理员
+const isAdmin = computed(() => {
+  const userType = localStorage.getItem('userType')
+  return userType === 'admin'
+})
+
+// 查询条件
+const searchForm = reactive({
+  code: '',
+  belongDept: '',
+  belongWard: '',
+  belongRoom: ''
+})
+
+// 下拉列表数据
+const deptList = ref([])
+const wardList = ref([])
+const roomList = ref([])
+
+// 表格数据
+const tableData = ref([])  
+
+// 加载状态
+const loading = ref(false)
+
+// 分页相关
+const currentPage = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const jumpPage = ref(1)
+
+// 计算总页数
+const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1)
+
+// 计算显示的页码
+const displayPages = computed(() => {
+  const pages = []
+  const maxShow = 5
+  let start = Math.max(1, currentPage.value - Math.floor(maxShow / 2))
+  let end = Math.min(totalPages.value, start + maxShow - 1)
+  if (end - start + 1 < maxShow) {
+    start = Math.max(1, end - maxShow + 1)
+  }
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  return pages
+})
+
+// 选中的行
+const selectedRows = ref([])
+const selectAll = computed({
+  get: () => tableData.value.length > 0 && selectedRows.value.length === tableData.value.length,
+  set: () => {}
+})
+
+// 详情弹窗
+const detailVisible = ref(false)
+const currentBed = ref({})
+
+// 代码设置弹窗
+const codeSettingVisible = ref(false)
+const codeSettingList = ref([])
+
+onMounted(() => {
+  loadDeptList()
+  loadWardList()
+  loadRoomList()
+  loadData()
+})
+
+// 加载所属科室列表
+const loadDeptList = async () => {
+  try {
+    const res = await request.get('/api/bed/dept-list')
+    if (res.code === 200) {
+      deptList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载科室列表失败:', error)
+  }
+}
+
+// 加载病区列表
+const loadWardList = async () => {
+  try {
+    const params = {}
+    if (searchForm.belongDept) {
+      params.belongDept = searchForm.belongDept
+    }
+    const res = await request.get('/api/bed/ward-list', { params })
+    if (res.code === 200) {
+      wardList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载病区列表失败:', error)
+  }
+}
+
+// 加载病房列表
+const loadRoomList = async () => {
+  try {
+    const params = {}
+    if (searchForm.belongWard) {
+      params.belongWard = searchForm.belongWard
+    }
+    const res = await request.get('/api/bed/room-list', { params })
+    if (res.code === 200) {
+      roomList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载病房列表失败:', error)
+  }
+}
+
+// 科室变化时
+const onDeptChange = () => {
+  searchForm.belongWard = ''
+  searchForm.belongRoom = ''
+  loadWardList()
+  roomList.value = []
+}
+
+// 病区变化时
+const onWardChange = () => {
+  searchForm.belongRoom = ''
+  loadRoomList()
+}
+
+// 加载数据
+const loadData = async () => {
+  loading.value = true
+  try {
+    const params = {
+      page: currentPage.value,
+      pageSize: pageSize.value
+    }
+    if (searchForm.code) params.code = searchForm.code
+    if (searchForm.belongDept) params.belongDept = searchForm.belongDept
+    if (searchForm.belongWard) params.belongWard = searchForm.belongWard
+    if (searchForm.belongRoom) params.belongRoom = searchForm.belongRoom
+    
+    const res = await request.get('/api/bed/list', { params })
+    if (res.code === 200) {
+      tableData.value = res.data?.records || res.data || []
+      total.value = res.data?.total || tableData.value.length
+      selectedRows.value = []
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 切换页码
+const changePage = (page) => {
+  if (page >= 1 && page <= totalPages.value) {
+    currentPage.value = page
+    jumpPage.value = page
+    loadData()
+  }
+}
+
+// 修改每页条数
+const handlePageSizeChange = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 跳转到指定页
+const handleJumpPage = () => {
+  const page = Math.min(Math.max(1, jumpPage.value), totalPages.value)
+  changePage(page)
+}
+
+// 判断是否选中
+const isSelected = (item) => {
+  return selectedRows.value.some(row => row.id === item.id)
+}
+
+// 切换选中状态
+const toggleSelect = (item) => {
+  const index = selectedRows.value.findIndex(row => row.id === item.id)
+  if (index > -1) {
+    selectedRows.value.splice(index, 1)
+  } else {
+    selectedRows.value.push(item)
+  }
+}
+
+// 全选/取消全选
+const handleSelectAll = (e) => {
+  if (e.target.checked) {
+    selectedRows.value = [...tableData.value]
+  } else {
+    selectedRows.value = []
+  }
+}
+
+// 查询
+const handleSearch = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 重置
+const handleReset = () => {
+  searchForm.code = ''
+  searchForm.belongDept = ''
+  searchForm.belongWard = ''
+  searchForm.belongRoom = ''
+  currentPage.value = 1
+  loadWardList()
+  loadRoomList()
+  loadData()
+}
+
+// 查看详情
+const showDetail = async (item) => {
+  try {
+    const res = await request.get(`/api/bed/detail/${item.id}`)
+    if (res.code === 200) {
+      currentBed.value = res.data
+      detailVisible.value = true
+    }
+  } catch (error) {
+    console.error('获取详情失败:', error)
+  }
+}
+
+// 打开代码设置弹窗
+const openCodeSetting = () => {
+  codeSettingList.value = selectedRows.value.map(item => ({
+    id: item.id,
+    name: item.name,
+    belongWard: item.belongWard,
+    code: item.code,
+    newCode: item.code || ''
+  }))
+  codeSettingVisible.value = true
+}
+
+// 格式化代码输入
+const formatCodeInput = (index) => {
+  let code = codeSettingList.value[index].newCode
+  if (code) {
+    code = code.replace(/\D/g, '')
+    if (code) {
+      let num = parseInt(code)
+      if (num > 999) num = 999
+      if (num < 0) num = 0
+      codeSettingList.value[index].newCode = String(num).padStart(3, '0')
+    }
+  }
+}
+
+// 保存代码设置
+const saveCodeSetting = async () => {
+  const ids = codeSettingList.value.map(item => item.id)
+  const codes = codeSettingList.value.map(item => item.newCode)
+  
+  try {
+    const res = await request.post('/api/bed/batch-set-code', { ids, codes })
+    if (res.code === 200) {
+      if (res.errors && res.errors.length > 0) {
+        alert('部分设置成功\n' + res.errors.join('\n'))
+      } else {
+        alert('设置成功')
+      }
+      codeSettingVisible.value = false
+      loadData()
+    } else {
+      alert(res.message || '设置失败')
+    }
+  } catch (error) {
+    console.error('设置失败:', error)
+    alert('设置失败')
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+}
+.page-header {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 15px;
+  margin-bottom: 20px;
+  font-size: 14px;
+}
+.breadcrumb-label { color: #666; }
+.breadcrumb-path { color: #666; }
+.breadcrumb-separator { margin: 0 5px; color: #666; }
+.breadcrumb-current { color: #409eff; }
+
+/* 查询栏 */
+.search-bar {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin-bottom: 15px;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
+.search-item {
+  display: flex;
+  align-items: center;
+}
+.search-item label {
+  margin-right: 8px;
+  color: #666;
+  font-size: 14px;
+  white-space: nowrap;
+}
+.search-item input,
+.search-item select {
+  padding: 8px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 14px;
+}
+.search-item input { width: 120px; }
+.search-item select { width: 120px; }
+.search-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+.search-btn:hover { background: #66b1ff; }
+.reset-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #666;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+.reset-btn:hover { border-color: #409eff; color: #409eff; }
+
+/* 操作按钮栏 */
+.action-bar {
+  margin-bottom: 15px;
+}
+.code-set-btn {
+  padding: 8px 20px;
+  background: #67c23a;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.code-set-btn:hover { background: #85ce61; }
+.code-set-btn:disabled {
+  background: #c0c4cc;
+  cursor: not-allowed;
+}
+
+/* 数据表格 */
+.table-container {
+  overflow-x: auto;
+}
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 14px;
+}
+.data-table th,
+.data-table td {
+  padding: 12px;
+  text-align: left;
+  border-bottom: 1px solid #eee;
+}
+.data-table th {
+  background: #f5f7fa;
+  font-weight: 500;
+  color: #333;
+}
+.data-table tbody tr:hover {
+  background: #f5f7fa;
+}
+.empty-tip {
+  text-align: center;
+  color: #999;
+  padding: 40px 0;
+}
+.detail-btn {
+  padding: 4px 12px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+}
+.detail-btn:hover { background: #66b1ff; }
+
+/* 加载状态 */
+.loading-mask {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 0;
+  color: #909399;
+}
+.loading-spinner {
+  width: 32px;
+  height: 32px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 12px;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+/* 弹窗样式 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+.modal-content {
+  background: #fff;
+  border-radius: 8px;
+  min-width: 500px;
+  max-width: 90%;
+  max-height: 90%;
+  overflow: hidden;
+}
+.code-setting-modal {
+  min-width: 700px;
+}
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 20px;
+  border-bottom: 1px solid #eee;
+  font-size: 16px;
+  font-weight: 500;
+}
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 20px;
+  cursor: pointer;
+  color: #999;
+}
+.close-btn:hover { color: #333; }
+.modal-body {
+  padding: 20px;
+  max-height: 60vh;
+  overflow-y: auto;
+}
+.modal-footer {
+  padding: 15px 20px;
+  border-top: 1px solid #eee;
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+/* 详情行 */
+.detail-row {
+  display: flex;
+  padding: 10px 0;
+  border-bottom: 1px solid #f0f0f0;
+}
+.detail-row .label {
+  width: 120px;
+  color: #666;
+  flex-shrink: 0;
+}
+.detail-row .value {
+  flex: 1;
+  color: #333;
+}
+
+/* 代码设置表格 */
+.code-setting-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 14px;
+}
+.code-setting-table th,
+.code-setting-table td {
+  padding: 10px;
+  text-align: left;
+  border: 1px solid #eee;
+}
+.code-setting-table th {
+  background: #f5f7fa;
+}
+.code-setting-table input {
+  width: 80px;
+  padding: 6px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+}
+.code-tips {
+  margin-top: 15px;
+  color: #999;
+  font-size: 12px;
+}
+
+/* 按钮 */
+.cancel-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #666;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.cancel-btn:hover { border-color: #409eff; color: #409eff; }
+.confirm-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.confirm-btn:hover { background: #66b1ff; }
+
+/* 分页样式 */
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  padding: 15px 0;
+  border-top: 1px solid #eee;
+}
+.pagination-info {
+  color: #666;
+  font-size: 14px;
+}
+.pagination-controls {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.page-btn {
+  padding: 6px 12px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-btn:hover:not(:disabled) {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-btn:disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+.page-numbers {
+  display: flex;
+  gap: 5px;
+}
+.page-num {
+  min-width: 32px;
+  height: 28px;
+  padding: 0 6px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-num:hover {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-num.active {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+}
+.page-size-selector select {
+  padding: 5px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 13px;
+  margin-left: 10px;
+}
+.page-jump {
+  font-size: 13px;
+  color: #606266;
+  margin-left: 10px;
+}
+.page-jump input {
+  width: 50px;
+  padding: 5px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  text-align: center;
+  margin: 0 5px;
+}
+</style>

+ 561 - 0
frontend/src/views/hospital/Department.vue

@@ -0,0 +1,561 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <span class="breadcrumb-label">当前位置:</span>
+      <span class="breadcrumb-path">院区信息管理</span>
+      <span class="breadcrumb-separator">&gt;</span>
+      <span class="breadcrumb-current">科室管理</span>
+    </div>
+    
+    <!-- 查询条件 -->
+    <div class="search-bar">
+      <div class="search-item">
+        <label>科室名称:</label>
+        <input v-model="searchForm.name" type="text" placeholder="请输入科室名称" />
+      </div>
+      <div class="search-item">
+        <label>是否启用:</label>
+        <select v-model="searchForm.enabled">
+          <option value="">全部</option>
+          <option :value="1">是</option>
+          <option :value="0">否</option>
+        </select>
+      </div>
+      <button class="search-btn" @click="handleSearch">查询</button>
+      <button class="reset-btn" @click="handleReset">重置</button>
+    </div>
+
+    <!-- 数据列表 -->
+    <div class="table-container" v-loading="loading">
+      <div v-if="loading" class="loading-mask">
+        <div class="loading-spinner"></div>
+        <span>加载中...</span>
+      </div>
+      <table class="data-table" v-show="!loading">
+        <thead>
+          <tr>
+            <th>科室代码</th>
+            <th>科室名称</th>
+            <th>科室地址</th>
+            <th>科室电话</th>
+            <th>科室主任</th>
+            <th>是否启用</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="item in tableData" :key="item.id">
+            <td>{{ item.code }}</td>
+            <td>{{ item.name }}</td>
+            <td>{{ item.address }}</td>
+            <td>{{ item.telephone }}</td>
+            <td>{{ item.director }}</td>
+            <td>{{ String(item.enabled) === '0' ? '不启用' : '启用' }}</td>
+            <td>
+              <button class="action-btn detail-btn" @click="showDetail(item)">详情</button>
+            </td>
+          </tr>
+          <tr v-if="tableData.length === 0">
+            <td colspan="7" class="no-data">暂无数据</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination" v-if="total > 0">
+      <span class="pagination-info">共 {{ total }} 条记录</span>
+      <div class="pagination-controls">
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(1)">首页</button>
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(currentPage - 1)">上一页</button>
+        <span class="page-numbers">
+          <button 
+            v-for="page in displayPages" 
+            :key="page" 
+            class="page-num" 
+            :class="{ active: page === currentPage }"
+            @click="changePage(page)"
+          >{{ page }}</button>
+        </span>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(currentPage + 1)">下一页</button>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(totalPages)">末页</button>
+        <span class="page-size-selector">
+          <select v-model="pageSize" @change="handlePageSizeChange">
+            <option :value="10">10条/页</option>
+            <option :value="20">20条/页</option>
+            <option :value="50">50条/页</option>
+            <option :value="100">100条/页</option>
+          </select>
+        </span>
+        <span class="page-jump">
+          跳至 <input type="number" v-model.number="jumpPage" min="1" :max="totalPages" @keyup.enter="handleJumpPage" /> 页
+        </span>
+      </div>
+    </div>
+
+    <!-- 详情弹窗 -->
+    <div class="modal-overlay" v-if="detailVisible" @click.self="detailVisible = false">
+      <div class="modal-content">
+        <div class="modal-header">
+          <h3>科室详情</h3>
+          <span class="close-btn" @click="detailVisible = false">&times;</span>
+        </div>
+        <div class="modal-body">
+          <div class="detail-row">
+            <label>科室代码:</label>
+            <span>{{ currentDept.code }}</span>
+          </div>
+          <div class="detail-row">
+            <label>科室外部代码:</label>
+            <span>{{ currentDept.outCode }}</span>
+          </div>
+          <div class="detail-row">
+            <label>科室名称:</label>
+            <span>{{ currentDept.name }}</span>
+          </div>
+          <div class="detail-row">
+            <label>科室地址:</label>
+            <span>{{ currentDept.address }}</span>
+          </div>
+          <div class="detail-row">
+            <label>科室电话:</label>
+            <span>{{ currentDept.telephone }}</span>
+          </div>
+          <div class="detail-row">
+            <label>科室主任:</label>
+            <span>{{ currentDept.director }}</span>
+          </div>
+          <div class="detail-row">
+            <label>是否启用:</label>
+            <span>{{ currentDept.enabled == 0 ? '不启用' : '启用' }}</span>
+          </div>
+          <div class="detail-row">
+            <label>备注:</label>
+            <span>{{ currentDept.remark }}</span>
+          </div>
+          <div class="detail-row intro-row">
+            <label>科室介绍:</label>
+            <div class="intro-content" v-html="currentDept.introduction"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from 'vue'
+import request from '@/utils/request'
+
+// 查询表单
+const searchForm = reactive({
+  name: '',
+  enabled: ''
+})
+
+// 表格数据
+const tableData = ref([])
+
+// 加载状态
+const loading = ref(false)
+
+// 分页相关
+const currentPage = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const jumpPage = ref(1)
+
+// 计算总页数
+const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1)
+
+// 计算显示的页码
+const displayPages = computed(() => {
+  const pages = []
+  const maxShow = 5
+  let start = Math.max(1, currentPage.value - Math.floor(maxShow / 2))
+  let end = Math.min(totalPages.value, start + maxShow - 1)
+  if (end - start + 1 < maxShow) {
+    start = Math.max(1, end - maxShow + 1)
+  }
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  return pages
+})
+
+// 详情弹窗
+const detailVisible = ref(false)
+const currentDept = ref({})
+
+onMounted(() => {
+  loadData()
+})
+
+// 加载数据
+const loadData = async () => {
+  loading.value = true
+  try {
+    const params = {
+      page: currentPage.value,
+      pageSize: pageSize.value
+    }
+    if (searchForm.name) params.name = searchForm.name
+    if (searchForm.enabled !== '') params.enabled = searchForm.enabled
+    
+    const res = await request.get('/api/department/list', { params })
+    if (res.code === 200) {
+      tableData.value = res.data?.records || res.data || []
+      total.value = res.data?.total || tableData.value.length
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 切换页码
+const changePage = (page) => {
+  if (page >= 1 && page <= totalPages.value) {
+    currentPage.value = page
+    jumpPage.value = page
+    loadData()
+  }
+}
+
+// 修改每页条数
+const handlePageSizeChange = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 跳转到指定页
+const handleJumpPage = () => {
+  const page = Math.min(Math.max(1, jumpPage.value), totalPages.value)
+  changePage(page)
+}
+
+// 查询
+const handleSearch = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 重置
+const handleReset = () => {
+  searchForm.name = ''
+  searchForm.enabled = ''
+  currentPage.value = 1
+  loadData()
+}
+
+// 查看详情
+const showDetail = async (item) => {
+  try {
+    const res = await request.get(`/api/department/detail/${item.id}`)
+    if (res.code === 200) {
+      currentDept.value = res.data
+      detailVisible.value = true
+    }
+  } catch (error) {
+    console.error('获取详情失败:', error)
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+}
+.page-header {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 15px;
+  margin-bottom: 20px;
+  font-size: 14px;
+}
+.breadcrumb-label {
+  color: #666;
+}
+.breadcrumb-path {
+  color: #666;
+}
+.breadcrumb-separator {
+  margin: 0 5px;
+  color: #666;
+}
+.breadcrumb-current {
+  color: #409eff;
+}
+
+/* 查询栏 */
+.search-bar {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  margin-bottom: 20px;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
+.search-item {
+  display: flex;
+  align-items: center;
+}
+.search-item label {
+  margin-right: 8px;
+  color: #666;
+  font-size: 14px;
+}
+.search-item input,
+.search-item select {
+  padding: 8px 12px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 14px;
+}
+.search-item input {
+  width: 180px;
+}
+.search-item select {
+  width: 120px;
+}
+.search-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.search-btn:hover {
+  background: #66b1ff;
+}
+.reset-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #606266;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.reset-btn:hover {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+
+/* 表格 */
+.table-container {
+  overflow-x: auto;
+}
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+.data-table th,
+.data-table td {
+  padding: 12px 15px;
+  text-align: left;
+  border-bottom: 1px solid #ebeef5;
+  font-size: 14px;
+}
+.data-table th {
+  background: #fafafa;
+  color: #909399;
+  font-weight: 500;
+}
+.data-table tr:hover {
+  background: #f5f7fa;
+}
+.no-data {
+  text-align: center;
+  color: #909399;
+  padding: 40px 0;
+}
+.action-btn {
+  padding: 5px 12px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+}
+.detail-btn {
+  background: #409eff;
+  color: #fff;
+}
+.detail-btn:hover {
+  background: #66b1ff;
+}
+
+/* 加载状态 */
+.loading-mask {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 0;
+  color: #909399;
+}
+.loading-spinner {
+  width: 32px;
+  height: 32px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 12px;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+/* 弹窗 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+.modal-content {
+  background: #fff;
+  border-radius: 8px;
+  width: 600px;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 20px;
+  border-bottom: 1px solid #eee;
+}
+.modal-header h3 {
+  margin: 0;
+  font-size: 16px;
+}
+.close-btn {
+  font-size: 24px;
+  cursor: pointer;
+  color: #909399;
+}
+.close-btn:hover {
+  color: #606266;
+}
+.modal-body {
+  padding: 20px;
+}
+.detail-row {
+  display: flex;
+  margin-bottom: 15px;
+  font-size: 14px;
+}
+.detail-row label {
+  width: 120px;
+  color: #909399;
+  flex-shrink: 0;
+}
+.detail-row span {
+  color: #333;
+}
+.intro-row {
+  flex-direction: column;
+}
+.intro-row label {
+  margin-bottom: 10px;
+}
+.intro-content {
+  padding: 10px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  min-height: 100px;
+}
+
+/* 分页样式 */
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  padding: 15px 0;
+  border-top: 1px solid #eee;
+}
+.pagination-info {
+  color: #666;
+  font-size: 14px;
+}
+.pagination-controls {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.page-btn {
+  padding: 6px 12px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-btn:hover:not(:disabled) {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-btn:disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+.page-numbers {
+  display: flex;
+  gap: 5px;
+}
+.page-num {
+  min-width: 32px;
+  height: 28px;
+  padding: 0 6px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-num:hover {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-num.active {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+}
+.page-size-selector select {
+  padding: 5px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 13px;
+  margin-left: 10px;
+}
+.page-jump {
+  font-size: 13px;
+  color: #606266;
+  margin-left: 10px;
+}
+.page-jump input {
+  width: 50px;
+  padding: 5px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  text-align: center;
+  margin: 0 5px;
+}
+</style>

+ 446 - 0
frontend/src/views/hospital/Overview.vue

@@ -0,0 +1,446 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <span class="breadcrumb-label">当前位置:</span>
+      <span class="breadcrumb-path">院区信息管理</span>
+      <span class="breadcrumb-separator">&gt;</span>
+      <span class="breadcrumb-current">医院概况</span>
+    </div>
+    <div class="page-content">
+      <!-- 加载状态 -->
+      <div v-if="loading" class="loading-mask">
+        <div class="loading-spinner"></div>
+        <span>加载中...</span>
+      </div>
+      <form class="hospital-form" @submit.prevent="handleSave" v-show="!loading">
+        <!-- 医院名称 -->
+        <div class="form-item required">
+          <label>医院名称</label>
+          <input 
+            v-model="form.name" 
+            type="text" 
+            placeholder="请输入医院名称" 
+            maxlength="100"
+          />
+        </div>
+
+        <!-- 医院Logo -->
+        <div class="form-item">
+          <label>医院Logo</label>
+          <div class="logo-section">
+            <!-- Logo展示框 -->
+            <div class="logo-display">
+              <div class="logo-box">
+                <img v-if="form.logo" :src="getLogoUrl(form.logo)" alt="医院Logo" />
+                <div v-else class="logo-placeholder">
+                  <span>暂无Logo</span>
+                </div>
+              </div>
+            </div>
+            <!-- 上传操作区 -->
+            <div class="logo-upload">
+              <input 
+                type="file" 
+                ref="fileInput"
+                accept=".jpg,.jpeg,.png" 
+                @change="handleFileChange"
+                style="display: none;"
+              />
+              <button type="button" class="upload-btn" @click="$refs.fileInput.click()">上传Logo</button>
+              <button type="button" class="remove-btn" v-if="form.logo" @click="removeLogo">删除Logo</button>
+              <p class="upload-tip">支持jpg、png格式,大小不超过10M</p>
+            </div>
+          </div>
+        </div>
+
+        <!-- 医院地址 -->
+        <div class="form-item">
+          <label>医院地址</label>
+          <input 
+            v-model="form.address" 
+            type="text" 
+            placeholder="请输入医院地址" 
+            maxlength="200"
+          />
+        </div>
+
+        <!-- 联系电话 -->
+        <div class="form-item">
+          <label>联系电话</label>
+          <input 
+            v-model="form.phone" 
+            type="text" 
+            placeholder="请输入联系电话" 
+            maxlength="50"
+          />
+        </div>
+
+        <!-- 医院介绍 -->
+        <div class="form-item">
+          <label>医院介绍</label>
+          <div class="editor-container">
+            <Toolbar
+              :editor="editorRef"
+              :defaultConfig="toolbarConfig"
+              mode="default"
+              style="border-bottom: 1px solid #ccc;"
+            />
+            <Editor
+              v-model="form.introduction"
+              :defaultConfig="editorConfig"
+              mode="default"
+              style="height: 300px; overflow-y: hidden;"
+              @onCreated="handleCreated"
+            />
+          </div>
+        </div>
+
+        <!-- 提交按钮 -->
+        <div class="form-actions">
+          <button type="submit" class="save-btn" :disabled="saving">
+            {{ saving ? '保存中...' : '保存' }}
+          </button>
+        </div>
+      </form>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount, shallowRef } from 'vue'
+import request from '@/utils/request'
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+import '@wangeditor/editor/dist/css/style.css'
+
+// 编辑器实例
+const editorRef = shallowRef()
+
+// 工具栏配置
+const toolbarConfig = {}
+
+// 编辑器配置
+const editorConfig = {
+  placeholder: '请输入医院介绍...',
+  MENU_CONF: {}
+}
+
+// 表单数据
+const form = reactive({
+  name: '',
+  logo: '',
+  address: '',
+  phone: '',
+  introduction: ''
+})
+
+const saving = ref(false)
+const loading = ref(false)
+const fileInput = ref(null)
+
+// 组件挂载时获取数据
+onMounted(async () => {
+  loading.value = true
+  try {
+    await loadHospitalInfo()
+  } finally {
+    loading.value = false
+  }
+})
+
+// 组件销毁前销毁编辑器
+onBeforeUnmount(() => {
+  const editor = editorRef.value
+  if (editor) {
+    editor.destroy()
+  }
+})
+
+// 编辑器创建回调
+const handleCreated = (editor) => {
+  editorRef.value = editor
+}
+
+// 加载医院信息
+const loadHospitalInfo = async () => {
+  try {
+    const res = await request.get('/api/hospital/info')
+    if (res.code === 200 && res.data) {
+      Object.assign(form, res.data)
+    }
+  } catch (error) {
+    console.error('加载医院信息失败:', error)
+  }
+}
+
+// 获取Logo完整URL
+const getLogoUrl = (logo) => {
+  if (!logo) return ''
+  if (logo.startsWith('http')) return logo
+  // 图片保存在前端 public/img 目录,可直接访问
+  return logo
+}
+
+// 删除Logo
+const removeLogo = () => {
+  form.logo = ''
+}
+
+// 文件选择处理
+const handleFileChange = async (e) => {
+  const file = e.target.files[0]
+  if (!file) return
+
+  // 检查文件大小
+  if (file.size > 10 * 1024 * 1024) {
+    alert('文件大小不能超过10M')
+    return
+  }
+
+  // 检查文件类型
+  const validTypes = ['image/jpeg', 'image/png', 'image/jpg']
+  if (!validTypes.includes(file.type)) {
+    alert('只支持jpg、png格式图片')
+    return
+  }
+
+  // 上传文件
+  const formData = new FormData()
+  formData.append('file', file)
+
+  try {
+    const res = await request.post('/api/hospital/upload-logo', formData, {
+      headers: { 'Content-Type': 'multipart/form-data' }
+    })
+    if (res.code === 200) {
+      form.logo = res.data
+    } else {
+      alert(res.message || '上传失败')
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+    alert('上传失败')
+  }
+
+  // 清空文件输入
+  e.target.value = ''
+}
+
+// 保存表单
+const handleSave = async () => {
+  if (!form.name) {
+    alert('请输入医院名称')
+    return
+  }
+
+  saving.value = true
+  try {
+    const res = await request.post('/api/hospital/save', form)
+    if (res.code === 200) {
+      alert('保存成功')
+    } else {
+      alert(res.message || '保存失败')
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    alert('保存失败')
+  } finally {
+    saving.value = false
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+}
+.page-header {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 15px;
+  margin-bottom: 20px;
+  font-size: 14px;
+}
+.breadcrumb-label {
+  color: #666;
+}
+.breadcrumb-path {
+  color: #666;
+}
+.breadcrumb-separator {
+  margin: 0 5px;
+  color: #666;
+}
+.breadcrumb-current {
+  color: #409eff;
+}
+
+/* 表单样式 */
+.hospital-form {
+  max-width: 800px;
+}
+
+.form-item {
+  margin-bottom: 24px;
+}
+
+.form-item label {
+  display: block;
+  margin-bottom: 8px;
+  color: #333;
+  font-size: 14px;
+}
+
+.form-item.required label::before {
+  content: '*';
+  color: #f56c6c;
+  margin-right: 4px;
+}
+
+.form-item input[type="text"] {
+  width: 100%;
+  padding: 10px 12px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 14px;
+  box-sizing: border-box;
+}
+
+.form-item input[type="text"]:focus {
+  outline: none;
+  border-color: #667eea;
+}
+
+/* Logo展示和上传 */
+.logo-section {
+  display: flex;
+  align-items: flex-start;
+  gap: 24px;
+}
+
+.logo-display {
+  flex-shrink: 0;
+}
+
+.logo-box {
+  width: 150px;
+  height: 150px;
+  border: 2px dashed #dcdfe6;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #fafafa;
+  overflow: hidden;
+}
+
+.logo-box img {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+
+.logo-placeholder {
+  text-align: center;
+  color: #c0c4cc;
+}
+
+.logo-placeholder span {
+  font-size: 14px;
+}
+
+.logo-upload {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 10px;
+}
+
+.logo-upload .upload-btn {
+  padding: 8px 20px;
+  background: #667eea;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.logo-upload .upload-btn:hover {
+  background: #5a6fd6;
+}
+
+.logo-upload .remove-btn {
+  padding: 8px 20px;
+  background: #f56c6c;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.logo-upload .remove-btn:hover {
+  background: #f45c5c;
+}
+
+.upload-tip {
+  margin: 0;
+  font-size: 12px;
+  color: #909399;
+}
+
+/* 编辑器 */
+.editor-container {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+/* 提交按钮 */
+.form-actions {
+  margin-top: 30px;
+}
+
+.save-btn {
+  padding: 12px 40px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  font-size: 16px;
+  cursor: pointer;
+}
+
+.save-btn:hover {
+  opacity: 0.9;
+}
+
+.save-btn:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+/* 加载状态 */
+.loading-mask {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 100px 0;
+  color: #909399;
+}
+.loading-spinner {
+  width: 32px;
+  height: 32px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 12px;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+</style>

+ 813 - 0
frontend/src/views/hospital/Room.vue

@@ -0,0 +1,813 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <span class="breadcrumb-label">当前位置:</span>
+      <span class="breadcrumb-path">院区信息管理</span>
+      <span class="breadcrumb-separator">&gt;</span>
+      <span class="breadcrumb-current">病房管理</span>
+    </div>
+
+    <!-- 查询栏 -->
+    <div class="search-bar">
+      <div class="search-item">
+        <label>病房代码:</label>
+        <input type="text" v-model="searchForm.code" placeholder="请输入病房代码" />
+      </div>
+      <div class="search-item">
+        <label>所属科室:</label>
+        <select v-model="searchForm.belongDept" @change="onDeptChange">
+          <option value="">全部</option>
+          <option v-for="(dept, index) in deptList" :key="index" :value="dept">{{ dept }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>所属病区:</label>
+        <select v-model="searchForm.belongWard">
+          <option value="">全部</option>
+          <option v-for="(ward, index) in wardList" :key="index" :value="ward">{{ ward }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>是否启用:</label>
+        <select v-model="searchForm.enabled">
+          <option value="">全部</option>
+          <option value="1">是</option>
+          <option value="0">否</option>
+        </select>
+      </div>
+      <button class="search-btn" @click="handleSearch">查询</button>
+      <button class="reset-btn" @click="handleReset">重置</button>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="action-bar" v-if="isAdmin">
+      <button class="code-set-btn" @click="openCodeSetting" :disabled="selectedRows.length === 0">
+        病房代码设置
+      </button>
+    </div>
+
+    <!-- 数据列表 -->
+    <div class="table-container">
+      <div v-if="loading" class="loading-mask">
+        <div class="loading-spinner"></div>
+        <span>加载中...</span>
+      </div>
+      <table class="data-table" v-show="!loading">
+        <thead>
+          <tr>
+            <th><input type="checkbox" :checked="selectAll" @change="handleSelectAll" /></th>
+            <th>病房代码</th>
+            <th>病房外部代码</th>
+            <th>病房名称</th>
+            <th>额定床位</th>
+            <th>所属科室</th>
+            <th>所属病区</th>
+            <th>是否启用</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="item in tableData" :key="item.id">
+            <td><input type="checkbox" :checked="isSelected(item)" @change="toggleSelect(item)" /></td>
+            <td>{{ item.code }}</td>
+            <td>{{ item.outCode }}</td>
+            <td>{{ item.name }}</td>
+            <td>{{ item.bedCount }}</td>
+            <td>{{ item.belongDept }}</td>
+            <td>{{ item.belongWard }}</td>
+            <td>{{ item.enabled === 1 ? '是' : '否' }}</td>
+            <td>
+              <button class="detail-btn" @click="showDetail(item)">详情</button>
+            </td>
+          </tr>
+          <tr v-if="tableData.length === 0">
+            <td colspan="9" class="empty-tip">暂无数据</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination" v-if="total > 0">
+      <span class="pagination-info">共 {{ total }} 条记录</span>
+      <div class="pagination-controls">
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(1)">首页</button>
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(currentPage - 1)">上一页</button>
+        <span class="page-numbers">
+          <button 
+            v-for="page in displayPages" 
+            :key="page" 
+            class="page-num" 
+            :class="{ active: page === currentPage }"
+            @click="changePage(page)"
+          >{{ page }}</button>
+        </span>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(currentPage + 1)">下一页</button>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(totalPages)">末页</button>
+        <span class="page-size-selector">
+          <select v-model="pageSize" @change="handlePageSizeChange">
+            <option :value="10">10条/页</option>
+            <option :value="20">20条/页</option>
+            <option :value="50">50条/页</option>
+            <option :value="100">100条/页</option>
+          </select>
+        </span>
+        <span class="page-jump">
+          跳至 <input type="number" v-model.number="jumpPage" min="1" :max="totalPages" @keyup.enter="handleJumpPage" /> 页
+        </span>
+      </div>
+    </div>
+
+    <!-- 详情弹窗 -->
+    <div class="modal-overlay" v-if="detailVisible" @click.self="detailVisible = false">
+      <div class="modal-content">
+        <div class="modal-header">
+          <span>病房详情</span>
+          <button class="close-btn" @click="detailVisible = false">×</button>
+        </div>
+        <div class="modal-body">
+          <div class="detail-row">
+            <span class="label">病房代码:</span>
+            <span class="value">{{ currentRoom.code }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="label">病房外部代码:</span>
+            <span class="value">{{ currentRoom.outCode }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="label">病房名称:</span>
+            <span class="value">{{ currentRoom.name }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="label">额定床位:</span>
+            <span class="value">{{ currentRoom.bedCount }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="label">所属科室:</span>
+            <span class="value">{{ currentRoom.belongDept }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="label">所属病区:</span>
+            <span class="value">{{ currentRoom.belongWard }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="label">排序号:</span>
+            <span class="value">{{ currentRoom.sort }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="label">是否启用:</span>
+            <span class="value">{{ currentRoom.enabled === 1 ? '是' : '否' }}</span>
+          </div>
+          <div class="detail-row">
+            <span class="label">备注:</span>
+            <span class="value">{{ currentRoom.remark }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 病房代码设置弹窗 -->
+    <div class="modal-overlay" v-if="codeSettingVisible" @click.self="codeSettingVisible = false">
+      <div class="modal-content code-setting-modal">
+        <div class="modal-header">
+          <span>病房代码设置</span>
+          <button class="close-btn" @click="codeSettingVisible = false">×</button>
+        </div>
+        <div class="modal-body">
+          <table class="code-setting-table">
+            <thead>
+              <tr>
+                <th>病房名称</th>
+                <th>所属病区</th>
+                <th>当前代码</th>
+                <th>新代码</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="(item, index) in codeSettingList" :key="item.id">
+                <td>{{ item.name }}</td>
+                <td>{{ item.belongWard }}</td>
+                <td>{{ item.code }}</td>
+                <td>
+                  <input type="text" v-model="item.newCode" maxlength="4" 
+                         placeholder="0001~9999" @blur="formatCodeInput(index)" />
+                </td>
+              </tr>
+            </tbody>
+          </table>
+          <div class="code-tips">
+            提示:病房代码为0001~9999四位数字,输入1或101将自动补0变为0001与0101,同病区下代码不可重复
+          </div>
+        </div>
+        <div class="modal-footer">
+          <button class="cancel-btn" @click="codeSettingVisible = false">取消</button>
+          <button class="confirm-btn" @click="saveCodeSetting">保存</button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, watch } from 'vue'
+import request from '@/utils/request'
+
+// 判断是否为管理员
+const isAdmin = computed(() => {
+  const userType = localStorage.getItem('userType')
+  return userType === 'admin'
+})
+
+// 查询条件
+const searchForm = reactive({
+  code: '',
+  belongDept: '',
+  belongWard: '',
+  enabled: ''
+})
+
+// 科室列表和病区列表
+const deptList = ref([])
+const wardList = ref([])
+
+// 表格数据
+const tableData = ref([])
+
+// 加载状态
+const loading = ref(false)
+
+// 分页相关
+const currentPage = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const jumpPage = ref(1)
+
+// 计算总页数
+const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1)
+
+// 计算显示的页码
+const displayPages = computed(() => {
+  const pages = []
+  const maxShow = 5
+  let start = Math.max(1, currentPage.value - Math.floor(maxShow / 2))
+  let end = Math.min(totalPages.value, start + maxShow - 1)
+  if (end - start + 1 < maxShow) {
+    start = Math.max(1, end - maxShow + 1)
+  }
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  return pages
+})
+
+// 选中的行
+const selectedRows = ref([])
+const selectAll = computed({
+  get: () => tableData.value.length > 0 && selectedRows.value.length === tableData.value.length,
+  set: () => {}
+})
+
+// 详情弹窗
+const detailVisible = ref(false)
+const currentRoom = ref({})
+
+// 代码设置弹窗
+const codeSettingVisible = ref(false)
+const codeSettingList = ref([])
+
+onMounted(() => {
+  loadDeptList()
+  loadWardList()
+  loadData()
+})
+
+// 加载所属科室列表
+const loadDeptList = async () => {
+  try {
+    const res = await request.get('/api/room/dept-list')
+    if (res.code === 200) {
+      deptList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载科室列表失败:', error)
+  }
+}
+
+// 根据科室加载病区列表
+const loadWardList = async () => {
+  try {
+    const params = {}
+    if (searchForm.belongDept) {
+      params.belongDept = searchForm.belongDept
+    }
+    const res = await request.get('/api/room/ward-list', { params })
+    if (res.code === 200) {
+      wardList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载病区列表失败:', error)
+  }
+}
+
+// 所属科室变化时,重新加载病区列表
+const onDeptChange = () => {
+  searchForm.belongWard = ''
+  loadWardList()
+}
+
+// 加载数据
+const loadData = async () => {
+  loading.value = true
+  try {
+    const params = {
+      page: currentPage.value,
+      pageSize: pageSize.value
+    }
+    if (searchForm.code) params.code = searchForm.code
+    if (searchForm.belongDept) params.belongDept = searchForm.belongDept
+    if (searchForm.belongWard) params.belongWard = searchForm.belongWard
+    if (searchForm.enabled !== '') params.enabled = searchForm.enabled
+    
+    const res = await request.get('/api/room/list', { params })
+    if (res.code === 200) {
+      tableData.value = res.data?.records || res.data || []
+      total.value = res.data?.total || tableData.value.length
+      selectedRows.value = []
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 切换页码
+const changePage = (page) => {
+  if (page >= 1 && page <= totalPages.value) {
+    currentPage.value = page
+    jumpPage.value = page
+    loadData()
+  }
+}
+
+// 修改每页条数
+const handlePageSizeChange = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 跳转到指定页
+const handleJumpPage = () => {
+  const page = Math.min(Math.max(1, jumpPage.value), totalPages.value)
+  changePage(page)
+}
+
+// 判断是否选中
+const isSelected = (item) => {
+  return selectedRows.value.some(row => row.id === item.id)
+}
+
+// 切换选中状态
+const toggleSelect = (item) => {
+  const index = selectedRows.value.findIndex(row => row.id === item.id)
+  if (index > -1) {
+    selectedRows.value.splice(index, 1)
+  } else {
+    selectedRows.value.push(item)
+  }
+}
+
+// 全选/取消全选
+const handleSelectAll = (e) => {
+  if (e.target.checked) {
+    selectedRows.value = [...tableData.value]
+  } else {
+    selectedRows.value = []
+  }
+}
+
+// 查询
+const handleSearch = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 重置
+const handleReset = () => {
+  searchForm.code = ''
+  searchForm.belongDept = ''
+  searchForm.belongWard = ''
+  searchForm.enabled = ''
+  currentPage.value = 1
+  loadWardList()
+  loadData()
+}
+
+// 查看详情
+const showDetail = async (item) => {
+  try {
+    const res = await request.get(`/api/room/detail/${item.id}`)
+    if (res.code === 200) {
+      currentRoom.value = res.data
+      detailVisible.value = true
+    }
+  } catch (error) {
+    console.error('获取详情失败:', error)
+  }
+}
+
+// 打开代码设置弹窗
+const openCodeSetting = () => {
+  codeSettingList.value = selectedRows.value.map(item => ({
+    id: item.id,
+    name: item.name,
+    belongWard: item.belongWard,
+    code: item.code,
+    newCode: item.code || ''
+  }))
+  codeSettingVisible.value = true
+}
+
+// 格式化代码输入
+const formatCodeInput = (index) => {
+  let code = codeSettingList.value[index].newCode
+  if (code) {
+    code = code.replace(/\D/g, '')
+    if (code) {
+      let num = parseInt(code)
+      if (num > 9999) num = 9999
+      if (num < 0) num = 0
+      codeSettingList.value[index].newCode = String(num).padStart(4, '0')
+    }
+  }
+}
+
+// 保存代码设置
+const saveCodeSetting = async () => {
+  const ids = codeSettingList.value.map(item => item.id)
+  const codes = codeSettingList.value.map(item => item.newCode)
+  
+  try {
+    const res = await request.post('/api/room/batch-set-code', { ids, codes })
+    if (res.code === 200) {
+      if (res.errors && res.errors.length > 0) {
+        alert('部分设置成功\n' + res.errors.join('\n'))
+      } else {
+        alert('设置成功')
+      }
+      codeSettingVisible.value = false
+      loadData()
+    } else {
+      alert(res.message || '设置失败')
+    }
+  } catch (error) {
+    console.error('设置失败:', error)
+    alert('设置失败')
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+}
+.page-header {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 15px;
+  margin-bottom: 20px;
+  font-size: 14px;
+}
+.breadcrumb-label { color: #666; }
+.breadcrumb-path { color: #666; }
+.breadcrumb-separator { margin: 0 5px; color: #666; }
+.breadcrumb-current { color: #409eff; }
+
+/* 查询栏 */
+.search-bar {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  margin-bottom: 15px;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
+.search-item {
+  display: flex;
+  align-items: center;
+}
+.search-item label {
+  margin-right: 8px;
+  color: #666;
+  font-size: 14px;
+  white-space: nowrap;
+}
+.search-item input,
+.search-item select {
+  padding: 8px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 14px;
+}
+.search-item input { width: 120px; }
+.search-item select { width: 120px; }
+.search-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+.search-btn:hover { background: #66b1ff; }
+.reset-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #666;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+.reset-btn:hover { border-color: #409eff; color: #409eff; }
+
+/* 操作按钮栏 */
+.action-bar {
+  margin-bottom: 15px;
+}
+.code-set-btn {
+  padding: 8px 20px;
+  background: #67c23a;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.code-set-btn:hover { background: #85ce61; }
+.code-set-btn:disabled {
+  background: #c0c4cc;
+  cursor: not-allowed;
+}
+
+/* 数据表格 */
+.table-container {
+  overflow-x: auto;
+}
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 14px;
+}
+.data-table th,
+.data-table td {
+  padding: 12px;
+  text-align: left;
+  border-bottom: 1px solid #eee;
+}
+.data-table th {
+  background: #f5f7fa;
+  font-weight: 500;
+  color: #333;
+}
+.data-table tbody tr:hover {
+  background: #f5f7fa;
+}
+.empty-tip {
+  text-align: center;
+  color: #999;
+  padding: 40px 0;
+}
+.detail-btn {
+  padding: 4px 12px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+}
+.detail-btn:hover { background: #66b1ff; }
+
+/* 加载状态 */
+.loading-mask {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 0;
+  color: #909399;
+}
+.loading-spinner {
+  width: 32px;
+  height: 32px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 12px;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+/* 弹窗样式 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+.modal-content {
+  background: #fff;
+  border-radius: 8px;
+  min-width: 500px;
+  max-width: 90%;
+  max-height: 90%;
+  overflow: hidden;
+}
+.code-setting-modal {
+  min-width: 700px;
+}
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 20px;
+  border-bottom: 1px solid #eee;
+  font-size: 16px;
+  font-weight: 500;
+}
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 20px;
+  cursor: pointer;
+  color: #999;
+}
+.close-btn:hover { color: #333; }
+.modal-body {
+  padding: 20px;
+  max-height: 60vh;
+  overflow-y: auto;
+}
+.modal-footer {
+  padding: 15px 20px;
+  border-top: 1px solid #eee;
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+/* 详情行 */
+.detail-row {
+  display: flex;
+  padding: 10px 0;
+  border-bottom: 1px solid #f0f0f0;
+}
+.detail-row .label {
+  width: 120px;
+  color: #666;
+  flex-shrink: 0;
+}
+.detail-row .value {
+  flex: 1;
+  color: #333;
+}
+
+/* 代码设置表格 */
+.code-setting-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 14px;
+}
+.code-setting-table th,
+.code-setting-table td {
+  padding: 10px;
+  text-align: left;
+  border: 1px solid #eee;
+}
+.code-setting-table th {
+  background: #f5f7fa;
+}
+.code-setting-table input {
+  width: 100px;
+  padding: 6px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+}
+.code-tips {
+  margin-top: 15px;
+  color: #999;
+  font-size: 12px;
+}
+
+/* 按钮 */
+.cancel-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #666;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.cancel-btn:hover { border-color: #409eff; color: #409eff; }
+.confirm-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.confirm-btn:hover { background: #66b1ff; }
+
+/* 分页样式 */
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  padding: 15px 0;
+  border-top: 1px solid #eee;
+}
+.pagination-info {
+  color: #666;
+  font-size: 14px;
+}
+.pagination-controls {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.page-btn {
+  padding: 6px 12px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-btn:hover:not(:disabled) {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-btn:disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+.page-numbers {
+  display: flex;
+  gap: 5px;
+}
+.page-num {
+  min-width: 32px;
+  height: 28px;
+  padding: 0 6px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-num:hover {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-num.active {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+}
+.page-size-selector select {
+  padding: 5px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 13px;
+  margin-left: 10px;
+}
+.page-jump {
+  font-size: 13px;
+  color: #606266;
+  margin-left: 10px;
+}
+.page-jump input {
+  width: 50px;
+  padding: 5px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  text-align: center;
+  margin: 0 5px;
+}
+</style>

+ 43 - 0
frontend/src/views/hospital/Ward.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <span class="breadcrumb-label">当前位置:</span>
+      <span class="breadcrumb-path">院区信息管理</span>
+      <span class="breadcrumb-separator">&gt;</span>
+      <span class="breadcrumb-current">病房管理</span>
+    </div>
+    <div class="page-content">
+      <p>病房管理页面内容</p>
+    </div>
+  </div>
+</template>
+
+<script setup>
+</script>
+
+<style scoped>
+.page-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+}
+.page-header {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 15px;
+  margin-bottom: 20px;
+  font-size: 14px;
+}
+.breadcrumb-label {
+  color: #666;
+}
+.breadcrumb-path {
+  color: #666;
+}
+.breadcrumb-separator {
+  margin: 0 5px;
+  color: #666;
+}
+.breadcrumb-current {
+  color: #409eff;
+}
+</style>

+ 726 - 0
frontend/src/views/hospital/WardArea.vue

@@ -0,0 +1,726 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <span class="breadcrumb-label">当前位置:</span>
+      <span class="breadcrumb-path">院区信息管理</span>
+      <span class="breadcrumb-separator">&gt;</span>
+      <span class="breadcrumb-current">病区管理</span>
+    </div>
+    
+    <!-- 查询条件 -->
+    <div class="search-bar">
+      <div class="search-item">
+        <label>病区名称:</label>
+        <input v-model="searchForm.name" type="text" placeholder="请输入病区名称" />
+      </div>
+      <div class="search-item">
+        <label>关联科室:</label>
+        <select v-model="searchForm.glkeshi">
+          <option value="">全部</option>
+          <option v-for="(dept, index) in glkeshiList" :key="index" :value="dept">{{ dept }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>是否启用:</label>
+        <select v-model="searchForm.enabled">
+          <option value="">全部</option>
+          <option value="1">是</option>
+          <option value="0">否</option>
+        </select>
+      </div>
+      <button class="search-btn" @click="handleSearch">查询</button>
+      <button class="reset-btn" @click="handleReset">重置</button>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="action-bar" v-if="isAdmin">
+      <button class="primary-btn" @click="openCodeSetting" :disabled="selectedRows.length === 0">病区代码设置</button>
+    </div>
+
+    <!-- 数据列表 -->
+    <div class="table-container">
+      <div v-if="loading" class="loading-mask">
+        <div class="loading-spinner"></div>
+        <span>加载中...</span>
+      </div>
+      <table class="data-table" v-show="!loading">
+        <thead>
+          <tr>
+            <th><input type="checkbox" v-model="selectAll" @change="handleSelectAll" /></th>
+            <th>病区代码</th>
+            <th>病区名称</th>
+            <th>关联科室</th>
+            <th>病区电话</th>
+            <th>主任医生</th>
+            <th>护士长</th>
+            <th>是否启用</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="item in tableData" :key="item.id">
+            <td><input type="checkbox" :value="item" v-model="selectedRows" /></td>
+            <td>{{ item.code }}</td>
+            <td>{{ item.name }}</td>
+            <td>{{ item.glkeshi }}</td>
+            <td>{{ item.telephone }}</td>
+            <td>{{ item.director }}</td>
+            <td>{{ item.headNurse }}</td>
+            <td>{{ item.enabled === 1 ? '是' : '否' }}</td>
+            <td>
+              <button class="action-btn detail-btn" @click="showDetail(item)">详情</button>
+            </td>
+          </tr>
+          <tr v-if="tableData.length === 0">
+            <td colspan="9" class="no-data">暂无数据</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination" v-if="total > 0">
+      <span class="pagination-info">共 {{ total }} 条记录</span>
+      <div class="pagination-controls">
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(1)">首页</button>
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(currentPage - 1)">上一页</button>
+        <span class="page-numbers">
+          <button 
+            v-for="page in displayPages" 
+            :key="page" 
+            class="page-num" 
+            :class="{ active: page === currentPage }"
+            @click="changePage(page)"
+          >{{ page }}</button>
+        </span>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(currentPage + 1)">下一页</button>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(totalPages)">末页</button>
+        <span class="page-size-selector">
+          <select v-model="pageSize" @change="handlePageSizeChange">
+            <option :value="10">10条/页</option>
+            <option :value="20">20条/页</option>
+            <option :value="50">50条/页</option>
+            <option :value="100">100条/页</option>
+          </select>
+        </span>
+        <span class="page-jump">
+          跳至 <input type="number" v-model.number="jumpPage" min="1" :max="totalPages" @keyup.enter="handleJumpPage" /> 页
+        </span>
+      </div>
+    </div>
+
+    <!-- 详情弹窗 -->
+    <div class="modal-overlay" v-if="detailVisible" @click.self="detailVisible = false">
+      <div class="modal-content">
+        <div class="modal-header">
+          <h3>病区详情</h3>
+          <span class="close-btn" @click="detailVisible = false">&times;</span>
+        </div>
+        <div class="modal-body">
+          <div class="detail-row"><label>病区代码:</label><span>{{ currentWard.code }}</span></div>
+          <div class="detail-row"><label>病区外部代码:</label><span>{{ currentWard.outCode }}</span></div>
+          <div class="detail-row"><label>病区名称:</label><span>{{ currentWard.name }}</span></div>
+          <div class="detail-row"><label>病区地址:</label><span>{{ currentWard.address }}</span></div>
+          <div class="detail-row"><label>关联科室:</label><span>{{ currentWard.glkeshi }}</span></div>
+          <div class="detail-row"><label>病区电话:</label><span>{{ currentWard.telephone }}</span></div>
+          <div class="detail-row"><label>主任医生:</label><span>{{ currentWard.director }}</span></div>
+          <div class="detail-row"><label>护士长:</label><span>{{ currentWard.headNurse }}</span></div>
+          <div class="detail-row"><label>编制床位数:</label><span>{{ currentWard.bedCount }}</span></div>
+          <div class="detail-row"><label>开放床位数:</label><span>{{ currentWard.bedOpenCount }}</span></div>
+          <div class="detail-row"><label>排序号:</label><span>{{ currentWard.sort }}</span></div>
+          <div class="detail-row"><label>是否启用:</label><span>{{ currentWard.enabled === 1 ? '是' : '否' }}</span></div>
+          <div class="detail-row"><label>备注:</label><span>{{ currentWard.remark }}</span></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 病区代码设置弹窗 -->
+    <div class="modal-overlay" v-if="codeSettingVisible" @click.self="codeSettingVisible = false">
+      <div class="modal-content">
+        <div class="modal-header">
+          <h3>病区代码设置</h3>
+          <span class="close-btn" @click="codeSettingVisible = false">&times;</span>
+        </div>
+        <div class="modal-body">
+          <p class="tip-text">病区代码规则:01~99二位数字,输入单位数自动补0</p>
+          <table class="code-table">
+            <thead>
+              <tr>
+                <th>病区名称</th>
+                <th>当前代码</th>
+                <th>新代码</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="(item, index) in codeSettingList" :key="item.id">
+                <td>{{ item.name }}</td>
+                <td>{{ item.code }}</td>
+                <td>
+                  <input 
+                    v-model="item.newCode" 
+                    type="text" 
+                    maxlength="2" 
+                    placeholder="01~99"
+                    @blur="formatCodeInput(index)"
+                  />
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+        <div class="modal-footer">
+          <button class="cancel-btn" @click="codeSettingVisible = false">取消</button>
+          <button class="confirm-btn" @click="saveCodeSetting">确定</button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from 'vue'
+import request from '@/utils/request'
+
+// 判断是否为管理员
+const isAdmin = computed(() => {
+  const userType = localStorage.getItem('userType')
+  return userType === 'admin'
+})
+
+// 查询表单
+const searchForm = reactive({
+  name: '',
+  glkeshi: '',
+  enabled: ''
+})
+
+// 科室列表
+const deptList = ref([])
+// 关联科室列表(来自病区表中的真实数据)
+const glkeshiList = ref([])
+
+// 表格数据
+const tableData = ref([])
+
+// 加载状态
+const loading = ref(false)
+
+// 分页相关
+const currentPage = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const jumpPage = ref(1)
+
+// 计算总页数
+const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1)
+
+// 计算显示的页码
+const displayPages = computed(() => {
+  const pages = []
+  const maxShow = 5
+  let start = Math.max(1, currentPage.value - Math.floor(maxShow / 2))
+  let end = Math.min(totalPages.value, start + maxShow - 1)
+  if (end - start + 1 < maxShow) {
+    start = Math.max(1, end - maxShow + 1)
+  }
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  return pages
+})
+
+// 选中的行
+const selectedRows = ref([])
+const selectAll = computed({
+  get: () => tableData.value.length > 0 && selectedRows.value.length === tableData.value.length,
+  set: () => {}
+})
+
+// 详情弹窗
+const detailVisible = ref(false)
+const currentWard = ref({})
+
+// 代码设置弹窗
+const codeSettingVisible = ref(false)
+const codeSettingList = ref([])
+
+onMounted(() => {
+  loadDeptList()
+  loadGlkeshiList()
+  loadData()
+})
+
+// 加载科室列表(只加载已启用的科室)
+const loadDeptList = async () => {
+  try {
+    const res = await request.get('/api/department/list', { params: { enabled: 1 } })
+    if (res.code === 200) {
+      // 处理分页结构
+      deptList.value = res.data?.records || res.data || []
+    }
+  } catch (error) {
+    console.error('加载科室列表失败:', error)
+  }
+}
+
+// 加载关联科室列表(来自病区表中的真实数据)
+const loadGlkeshiList = async () => {
+  try {
+    const res = await request.get('/api/wardarea/glkeshi-list')
+    console.log('关联科室列表返回:', res)
+    if (res.code === 200) {
+      glkeshiList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载关联科室列表失败:', error)
+  }
+}
+
+// 加载数据
+const loadData = async () => {
+  loading.value = true
+  try {
+    const params = {
+      page: currentPage.value,
+      pageSize: pageSize.value
+    }
+    if (searchForm.name) params.name = searchForm.name
+    if (searchForm.glkeshi) params.glkeshi = searchForm.glkeshi
+    if (searchForm.enabled) params.enabled = searchForm.enabled
+    
+    const res = await request.get('/api/wardarea/list', { params })
+    if (res.code === 200) {
+      tableData.value = res.data?.records || res.data || []
+      total.value = res.data?.total || tableData.value.length
+      selectedRows.value = []
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 切换页码
+const changePage = (page) => {
+  if (page >= 1 && page <= totalPages.value) {
+    currentPage.value = page
+    jumpPage.value = page
+    loadData()
+  }
+}
+
+// 修改每页条数
+const handlePageSizeChange = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 跳转到指定页
+const handleJumpPage = () => {
+  const page = Math.min(Math.max(1, jumpPage.value), totalPages.value)
+  changePage(page)
+}
+
+// 全选/取消全选
+const handleSelectAll = (e) => {
+  if (e.target.checked) {
+    selectedRows.value = [...tableData.value]
+  } else {
+    selectedRows.value = []
+  }
+}
+
+// 查询
+const handleSearch = () => {
+  currentPage.value = 1
+  loadData()
+}
+
+// 重置
+const handleReset = () => {
+  searchForm.name = ''
+  searchForm.glkeshi = ''
+  searchForm.enabled = ''
+  currentPage.value = 1
+  loadData()
+}
+
+// 查看详情
+const showDetail = async (item) => {
+  try {
+    const res = await request.get(`/api/wardarea/detail/${item.id}`)
+    if (res.code === 200) {
+      currentWard.value = res.data
+      detailVisible.value = true
+    }
+  } catch (error) {
+    console.error('获取详情失败:', error)
+  }
+}
+
+// 打开代码设置弹窗
+const openCodeSetting = () => {
+  codeSettingList.value = selectedRows.value.map(item => ({
+    id: item.id,
+    name: item.name,
+    code: item.code,
+    newCode: item.code || ''
+  }))
+  codeSettingVisible.value = true
+}
+
+// 格式化代码输入
+const formatCodeInput = (index) => {
+  let code = codeSettingList.value[index].newCode
+  if (code) {
+    code = code.replace(/\D/g, '')
+    if (code) {
+      let num = parseInt(code)
+      if (num > 99) num = 99
+      if (num < 0) num = 0
+      codeSettingList.value[index].newCode = String(num).padStart(2, '0')
+    }
+  }
+}
+
+// 保存代码设置
+const saveCodeSetting = async () => {
+  const ids = codeSettingList.value.map(item => item.id)
+  const codes = codeSettingList.value.map(item => item.newCode)
+  
+  try {
+    const res = await request.post('/api/wardarea/batch-set-code', { ids, codes })
+    if (res.code === 200) {
+      if (res.errors && res.errors.length > 0) {
+        alert('部分设置成功\n' + res.errors.join('\n'))
+      } else {
+        alert('设置成功')
+      }
+      codeSettingVisible.value = false
+      loadData()
+    } else {
+      alert(res.message || '设置失败')
+    }
+  } catch (error) {
+    console.error('设置失败:', error)
+    alert('设置失败')
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+}
+.page-header {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 15px;
+  margin-bottom: 20px;
+  font-size: 14px;
+}
+.breadcrumb-label { color: #666; }
+.breadcrumb-path { color: #666; }
+.breadcrumb-separator { margin: 0 5px; color: #666; }
+.breadcrumb-current { color: #409eff; }
+
+/* 查询栏 */
+.search-bar {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  margin-bottom: 15px;
+  padding: 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  flex-wrap: wrap;
+}
+.search-item {
+  display: flex;
+  align-items: center;
+}
+.search-item label {
+  margin-right: 8px;
+  color: #666;
+  font-size: 14px;
+  white-space: nowrap;
+}
+.search-item input,
+.search-item select {
+  padding: 8px 12px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 14px;
+}
+.search-item input { width: 180px; }
+.search-item select { width: 150px; }
+.search-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.search-btn:hover { background: #66b1ff; }
+.reset-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #606266;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.reset-btn:hover { color: #409eff; border-color: #c6e2ff; }
+
+/* 操作栏 */
+.action-bar {
+  margin-bottom: 15px;
+}
+.primary-btn {
+  padding: 8px 20px;
+  background: #67c23a;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.primary-btn:hover { background: #85ce61; }
+.primary-btn:disabled {
+  background: #c0c4cc;
+  cursor: not-allowed;
+}
+
+/* 表格 */
+.table-container { overflow-x: auto; }
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+.data-table th,
+.data-table td {
+  padding: 12px 15px;
+  text-align: left;
+  border-bottom: 1px solid #ebeef5;
+  font-size: 14px;
+}
+.data-table th {
+  background: #fafafa;
+  color: #909399;
+  font-weight: 500;
+}
+.data-table tr:hover { background: #f5f7fa; }
+.no-data {
+  text-align: center;
+  color: #909399;
+  padding: 40px 0;
+}
+.action-btn {
+  padding: 5px 12px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+}
+.detail-btn { background: #409eff; color: #fff; }
+.detail-btn:hover { background: #66b1ff; }
+
+/* 加载状态 */
+.loading-mask {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 0;
+  color: #909399;
+}
+.loading-spinner {
+  width: 32px;
+  height: 32px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 12px;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+/* 弹窗 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+.modal-content {
+  background: #fff;
+  border-radius: 8px;
+  width: 600px;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 20px;
+  border-bottom: 1px solid #eee;
+}
+.modal-header h3 { margin: 0; font-size: 16px; }
+.close-btn {
+  font-size: 24px;
+  cursor: pointer;
+  color: #909399;
+}
+.close-btn:hover { color: #606266; }
+.modal-body { padding: 20px; }
+.modal-footer {
+  padding: 15px 20px;
+  border-top: 1px solid #eee;
+  text-align: right;
+}
+.detail-row {
+  display: flex;
+  margin-bottom: 15px;
+  font-size: 14px;
+}
+.detail-row label {
+  width: 120px;
+  color: #909399;
+  flex-shrink: 0;
+}
+.detail-row span { color: #333; }
+
+/* 代码设置 */
+.tip-text {
+  color: #e6a23c;
+  font-size: 13px;
+  margin-bottom: 15px;
+}
+.code-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+.code-table th,
+.code-table td {
+  padding: 10px;
+  border: 1px solid #ebeef5;
+  text-align: center;
+}
+.code-table th { background: #fafafa; }
+.code-table input {
+  width: 80px;
+  padding: 5px;
+  text-align: center;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+}
+.cancel-btn {
+  padding: 8px 20px;
+  background: #fff;
+  color: #606266;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  margin-right: 10px;
+}
+.confirm-btn {
+  padding: 8px 20px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+/* 分页样式 */
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  padding: 15px 0;
+  border-top: 1px solid #eee;
+}
+.pagination-info {
+  color: #666;
+  font-size: 14px;
+}
+.pagination-controls {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.page-btn {
+  padding: 6px 12px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-btn:hover:not(:disabled) {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-btn:disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+.page-numbers {
+  display: flex;
+  gap: 5px;
+}
+.page-num {
+  min-width: 32px;
+  height: 28px;
+  padding: 0 6px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-num:hover {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-num.active {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+}
+.page-size-selector select {
+  padding: 5px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 13px;
+  margin-left: 10px;
+}
+.page-jump {
+  font-size: 13px;
+  color: #606266;
+  margin-left: 10px;
+}
+.page-jump input {
+  width: 50px;
+  padding: 5px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  text-align: center;
+  margin: 0 5px;
+}
+</style>

+ 1328 - 0
frontend/src/views/patient/Index.vue

@@ -0,0 +1,1328 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <span class="breadcrumb-label">当前位置:</span>
+      <span class="breadcrumb-path">院区信息管理</span>
+      <span class="breadcrumb-separator">&gt;</span>
+      <span class="breadcrumb-current">患者管理</span>
+    </div>
+
+    <!-- 查询栏第一行 -->
+    <div class="search-bar">
+      <div class="search-item">
+        <label>患者姓名:</label>
+        <input type="text" v-model="searchForm.name" placeholder="请输入患者姓名" />
+      </div>
+      <div class="search-item">
+        <label>所属科室:</label>
+        <select v-model="searchForm.deptCode" @change="onDeptChange">
+          <option value="">全部</option>
+          <option v-for="(dept, index) in deptList" :key="index" :value="dept">{{ dept }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>所属病区:</label>
+        <select v-model="searchForm.wardCode" @change="onWardChange">
+          <option value="">全部</option>
+          <option v-for="(ward, index) in wardList" :key="index" :value="ward">{{ ward }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>房间号:</label>
+        <select v-model="searchForm.roomNo" @change="onRoomChange">
+          <option value="">全部</option>
+          <option v-for="(room, index) in roomList" :key="index" :value="room">{{ room }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>床位号:</label>
+        <select v-model="searchForm.bedNo">
+          <option value="">全部</option>
+          <option v-for="(bed, index) in bedList" :key="index" :value="bed">{{ bed }}</option>
+        </select>
+      </div>
+    </div>
+
+    <!-- 查询栏第二行 -->
+    <div class="search-bar">
+      <div class="search-item">
+        <label>住院号:</label>
+        <input type="text" v-model="searchForm.inpatientNo" placeholder="请输入住院号" />
+      </div>
+      <div class="search-item">
+        <label>护理级别:</label>
+        <select v-model="searchForm.nurseLevel">
+          <option value="">全部</option>
+          <option v-for="(level, index) in nurseLevelList" :key="index" :value="level">{{ formatNurseLevel(level) }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>在院状态:</label>
+        <select v-model="searchForm.inpatientStatus">
+          <option value="">全部</option>
+          <option v-for="(status, index) in inpatientStatusList" :key="index" :value="status">{{ formatInpatientStatus(status) }}</option>
+        </select>
+      </div>
+      <div class="search-item">
+        <label>入院时间:</label>
+        <input type="date" v-model="searchForm.admissionDateStart" />
+        <span class="date-sep">~</span>
+        <input type="date" v-model="searchForm.admissionDateEnd" />
+      </div>
+      <button class="search-btn" @click="handleSearch">查询</button>
+      <button class="reset-btn" @click="handleReset">重置</button>
+    </div>
+
+    <!-- 数据列表 -->
+    <div class="table-container">
+      <div v-if="loading" class="loading-mask">
+        <div class="loading-spinner"></div>
+        <span>加载中...</span>
+      </div>
+      <table class="data-table" v-show="!loading">
+        <thead>
+          <tr>
+            <th>患者姓名</th>
+            <th>性别</th>
+            <th>年龄</th>
+            <th>住院号</th>
+            <th>所属科室</th>
+            <th>所属病区</th>
+            <th>房间号</th>
+            <th>床位号</th>
+            <th>护理级别</th>
+            <th>在院状态</th>
+            <th>入院时间</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="item in tableData" :key="item.id">
+            <td>{{ item.name }}</td>
+            <td>{{ item.sex }}</td>
+            <td>{{ item.age }}</td>
+            <td>{{ item.inpatientNo }}</td>
+            <td>{{ item.deptCode }}</td>
+            <td>{{ item.wardCode }}</td>
+            <td>{{ item.roomNo }}</td>
+            <td>{{ item.bedNo }}</td>
+            <td>{{ formatNurseLevel(item.nurseLevel) }}</td>
+            <td>{{ formatInpatientStatus(item.inpatientStatus) }}</td>
+            <td>{{ formatDate(item.admissionDateTime) }}</td>
+            <td>
+              <button class="detail-btn" @click="showDetail(item)">详情</button>
+            </td>
+          </tr>
+          <tr v-if="tableData.length === 0">
+            <td colspan="12" class="empty-tip">暂无数据</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination" v-if="total > 0">
+      <span class="pagination-info">共 {{ total }} 条记录</span>
+      <div class="pagination-controls">
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(1)">首页</button>
+        <button class="page-btn" :disabled="currentPage === 1" @click="changePage(currentPage - 1)">上一页</button>
+        <span class="page-numbers">
+          <button 
+            v-for="page in displayPages" 
+            :key="page" 
+            class="page-num" 
+            :class="{ active: page === currentPage }"
+            @click="changePage(page)"
+          >{{ page }}</button>
+        </span>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(currentPage + 1)">下一页</button>
+        <button class="page-btn" :disabled="currentPage === totalPages" @click="changePage(totalPages)">末页</button>
+        <span class="page-size-selector">
+          <select v-model="pageSize" @change="handlePageSizeChange">
+            <option :value="10">10条/页</option>
+            <option :value="20">20条/页</option>
+            <option :value="50">50条/页</option>
+            <option :value="100">100条/页</option>
+          </select>
+        </span>
+        <span class="page-jump">
+          跳至 <input type="number" v-model.number="jumpPage" min="1" :max="totalPages" @keyup.enter="handleJumpPage" /> 页
+        </span>
+      </div>
+    </div>
+
+    <!-- 详情弹窗 -->
+    <div class="modal-overlay" v-if="detailVisible" @click.self="detailVisible = false">
+      <div class="modal-content detail-modal">
+        <div class="modal-header">
+          <span>患者详情 - {{ currentPatient.name }}</span>
+          <button class="close-btn" @click="detailVisible = false">×</button>
+        </div>
+        <div class="modal-body">
+          <!-- 主页签 -->
+          <div class="tab-bar">
+            <div class="tab-item" :class="{ active: activeTab === 'basic' }" @click="activeTab = 'basic'">基本信息</div>
+            <div class="tab-item" :class="{ active: activeTab === 'diagnose' }" @click="activeTab = 'diagnose'">诊断信息</div>
+            <div class="tab-item" :class="{ active: activeTab === 'order' }" @click="activeTab = 'order'">医嘱信息</div>
+            <div class="tab-item" :class="{ active: activeTab === 'fee' }" @click="activeTab = 'fee'">费用信息</div>
+            <div class="tab-item" :class="{ active: activeTab === 'exam' }" @click="activeTab = 'exam'">检查检验</div>
+          </div>
+
+          <!-- 基本信息页面 -->
+          <div class="tab-content" v-show="activeTab === 'basic'">
+            <!-- 个人信息 -->
+            <div class="info-group">
+              <div class="group-title">个人信息</div>
+              <div class="detail-grid">
+                <div class="detail-item"><span class="label">患者姓名:</span><span class="value">{{ currentPatient.name }}</span></div>
+                <div class="detail-item"><span class="label">性别:</span><span class="value">{{ currentPatient.sex }}</span></div>
+                <div class="detail-item"><span class="label">年龄:</span><span class="value">{{ currentPatient.age }}</span></div>
+                <div class="detail-item"><span class="label">医保类型:</span><span class="value">{{ currentPatient.medicalInsuranceType }}</span></div>
+              </div>
+            </div>
+
+            <!-- 住院信息 -->
+            <div class="info-group">
+              <div class="group-title">住院信息</div>
+              <div class="detail-grid">
+                <div class="detail-item"><span class="label">住院号:</span><span class="value">{{ currentPatient.inpatientNo }}</span></div>
+                <div class="detail-item"><span class="label">住院流水号:</span><span class="value">{{ currentPatient.inpatientSerialNo }}</span></div>
+                <div class="detail-item"><span class="label">所属科室:</span><span class="value">{{ currentPatient.deptCode }}</span></div>
+                <div class="detail-item"><span class="label">所属病区:</span><span class="value">{{ currentPatient.wardCode }}</span></div>
+                <div class="detail-item"><span class="label">房间号:</span><span class="value">{{ currentPatient.roomNo }}</span></div>
+                <div class="detail-item"><span class="label">床位号:</span><span class="value">{{ currentPatient.bedNo }}</span></div>
+                <div class="detail-item"><span class="label">在院状态:</span><span class="value">{{ formatInpatientStatus(currentPatient.inpatientStatus) }}</span></div>
+                <div class="detail-item"><span class="label">入院时间:</span><span class="value">{{ formatDate(currentPatient.admissionDateTime) }}</span></div>
+                <div class="detail-item"><span class="label">入科时间:</span><span class="value">{{ formatDate(currentPatient.deptDateTime) }}</span></div>
+                <div class="detail-item"><span class="label">出院时间:</span><span class="value">{{ formatDate(currentPatient.dischargedDateTime) }}</span></div>
+              </div>
+            </div>
+
+            <!-- 医护信息 -->
+            <div class="info-group">
+              <div class="group-title">医护信息</div>
+              <div class="detail-grid">
+                <div class="detail-item"><span class="label">责任医生:</span><span class="value">{{ currentPatient.doctorCode }}</span></div>
+                <div class="detail-item"><span class="label">责任护士:</span><span class="value">{{ currentPatient.nurseCode }}</span></div>
+                <div class="detail-item"><span class="label">护理级别:</span><span class="value">{{ formatNurseLevel(currentPatient.nurseLevel) }}</span></div>
+                <div class="detail-item"><span class="label">病情状况:</span><span class="value">{{ currentPatient.conditionStatus }}</span></div>
+              </div>
+            </div>
+
+            <!-- 诊疗信息 -->
+            <div class="info-group">
+              <div class="group-title">诊疗信息</div>
+              <div class="detail-grid">
+                <div class="detail-item full-width"><span class="label">入院诊断:</span><span class="value">{{ currentPatient.diagnose }}</span></div>
+                <div class="detail-item full-width"><span class="label">过敏信息:</span><span class="value">{{ currentPatient.allergen }}</span></div>
+                <div class="detail-item"><span class="label">饮食状况:</span><span class="value">{{ currentPatient.dietType }}</span></div>
+                <div class="detail-item full-width"><span class="label">安全防范:</span><span class="value">{{ currentPatient.notice }}</span></div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 诊断信息页面 -->
+          <div class="tab-content" v-show="activeTab === 'diagnose'">
+            <table class="info-table">
+              <thead>
+                <tr>
+                  <th>诊断时间</th>
+                  <th>诊断类型</th>
+                  <th>诊断描述</th>
+                  <th>诊断状态</th>
+                  <th>诊断医生</th>
+                  <th>备注</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(item, index) in diagnoseList" :key="index">
+                  <td>{{ formatDateTime(item.diagnoseTime) }}</td>
+                  <td>{{ item.diagnoseType }}</td>
+                  <td>{{ item.diagnoseDesc }}</td>
+                  <td>{{ formatDiagnoseStatus(item.diagnoseStatus) }}</td>
+                  <td>{{ item.diagnoseDoctor }}</td>
+                  <td>{{ item.remark }}</td>
+                </tr>
+                <tr v-if="diagnoseList.length === 0"><td colspan="6" class="empty-tip">暂无数据</td></tr>
+              </tbody>
+            </table>
+          </div>
+
+          <!-- 医嘱信息页面 -->
+          <div class="tab-content" v-show="activeTab === 'order'">
+            <table class="info-table">
+              <thead>
+                <tr>
+                  <th>医嘱类型</th>
+                  <th>医嘱名称</th>
+                  <th>药物</th>
+                  <th>用量</th>
+                  <th>单位</th>
+                  <th>用药方式</th>
+                  <th>用药频率</th>
+                  <th>执行状态</th>
+                  <th>开始时间</th>
+                  <th>结束时间</th>
+                  <th>开嘱医生</th>
+                  <th>开嘱时间</th>
+                  <th>停嘱医生</th>
+                  <th>停嘱时间</th>
+                  <th>备注</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(item, index) in orderList" :key="index">
+                  <td>{{ formatOrderType(item.orderType) }}</td>
+                  <td>{{ item.orderName }}</td>
+                  <td>{{ item.medicine }}</td>
+                  <td>{{ item.dosage }}</td>
+                  <td>{{ item.dosageUnit }}</td>
+                  <td>{{ item.medicationWay }}</td>
+                  <td>{{ item.medicationFreq }}</td>
+                  <td>{{ formatExecStatus(item.execStatus) }}</td>
+                  <td>{{ formatDateTime(item.startTime) }}</td>
+                  <td>{{ formatDateTime(item.endTime) }}</td>
+                  <td>{{ item.orderDoctor }}</td>
+                  <td>{{ formatDateTime(item.orderTime) }}</td>
+                  <td>{{ item.stopDoctor }}</td>
+                  <td>{{ formatDateTime(item.stopTime) }}</td>
+                  <td>{{ item.remark }}</td>
+                </tr>
+                <tr v-if="orderList.length === 0"><td colspan="15" class="empty-tip">暂无数据</td></tr>
+              </tbody>
+            </table>
+          </div>
+
+          <!-- 费用信息页面 -->
+          <div class="tab-content" v-show="activeTab === 'fee'">
+            <table class="info-table">
+              <thead>
+                <tr>
+                  <th>费用产生日期</th>
+                  <th>项目名称</th>
+                  <th>金额</th>
+                  <th>数量</th>
+                  <th>单位</th>
+                  <th>小计</th>
+                  <th>备注</th>
+                  <th>是否缴费</th>
+                  <th>缴费时间</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(item, index) in feeList" :key="index">
+                  <td>{{ formatDateTime(item.chargeDate) }}</td>
+                  <td>{{ item.itemName }}</td>
+                  <td>{{ item.amount }}</td>
+                  <td>{{ item.quantity }}</td>
+                  <td>{{ item.unit }}</td>
+                  <td>{{ item.subtotal }}</td>
+                  <td>{{ item.remark }}</td>
+                  <td>{{ item.isPaid === 1 ? '是' : '否' }}</td>
+                  <td>{{ formatDateTime(item.payTime) }}</td>
+                </tr>
+                <tr v-if="feeList.length === 0"><td colspan="9" class="empty-tip">暂无数据</td></tr>
+              </tbody>
+            </table>
+          </div>
+
+          <!-- 检查检验页面 -->
+          <div class="tab-content" v-show="activeTab === 'exam'">
+            <!-- 子页签 -->
+            <div class="sub-tab-bar">
+              <div class="sub-tab-item" :class="{ active: examSubTab === 'check' }" @click="examSubTab = 'check'">检查报告</div>
+              <div class="sub-tab-item" :class="{ active: examSubTab === 'lab' }" @click="examSubTab = 'lab'">检验报告</div>
+            </div>
+
+            <!-- 检查报告 -->
+            <div v-show="examSubTab === 'check'">
+              <table class="info-table">
+                <thead>
+                  <tr>
+                    <th>检查名称</th>
+                    <th>检查部位</th>
+                    <th>检查方式</th>
+                    <th>光镜观察</th>
+                    <th>影像所见</th>
+                    <th>临床诊断</th>
+                    <th>影像诊断</th>
+                    <th>检查医生</th>
+                    <th>检查时间</th>
+                    <th>审核者</th>
+                    <th>报告时间</th>
+                    <th>备注</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="(item, index) in checkList" :key="index">
+                    <td>{{ item.examName }}</td>
+                    <td>{{ item.examPart }}</td>
+                    <td>{{ item.examMethod }}</td>
+                    <td>{{ item.microscopy }}</td>
+                    <td>{{ item.imagingFindings }}</td>
+                    <td>{{ item.clinicalDiagnosis }}</td>
+                    <td>{{ item.imagingDiagnosis }}</td>
+                    <td>{{ item.examDoctor }}</td>
+                    <td>{{ formatDateTime(item.examTime) }}</td>
+                    <td>{{ item.examiner }}</td>
+                    <td>{{ formatDateTime(item.reportTime) }}</td>
+                    <td>{{ item.remark }}</td>
+                  </tr>
+                  <tr v-if="checkList.length === 0"><td colspan="12" class="empty-tip">暂无数据</td></tr>
+                </tbody>
+              </table>
+            </div>
+
+            <!-- 检验报告 -->
+            <div v-show="examSubTab === 'lab'">
+              <table class="info-table">
+                <thead>
+                  <tr>
+                    <th>检验名称</th>
+                    <th>样品名称</th>
+                    <th>样品编号</th>
+                    <th>检验医生</th>
+                    <th>检验时间</th>
+                    <th>审核者</th>
+                    <th>报告时间</th>
+                    <th>备注</th>
+                    <th>操作</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="(item, index) in labList" :key="index">
+                    <td>{{ item.testName }}</td>
+                    <td>{{ item.sampleName }}</td>
+                    <td>{{ item.sampleCode }}</td>
+                    <td>{{ item.testDoctor }}</td>
+                    <td>{{ formatDateTime(item.testTime) }}</td>
+                    <td>{{ item.tester }}</td>
+                    <td>{{ formatDateTime(item.reportTime) }}</td>
+                    <td>{{ item.remark }}</td>
+                    <td><button class="detail-btn" @click="showLabDetail(item)">详情</button></td>
+                  </tr>
+                  <tr v-if="labList.length === 0"><td colspan="9" class="empty-tip">暂无数据</td></tr>
+                </tbody>
+              </table>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 检验报告详情弹窗 -->
+    <div class="modal-overlay" v-if="labDetailVisible" @click.self="labDetailVisible = false">
+      <div class="modal-content lab-detail-modal">
+        <div class="modal-header">
+          <span>检验报告详情 - {{ currentLab.testName }}</span>
+          <button class="close-btn" @click="labDetailVisible = false">×</button>
+        </div>
+        <div class="modal-body">
+          <table class="info-table">
+            <thead>
+              <tr>
+                <th>项目名称</th>
+                <th>英文缩写</th>
+                <th>结果值</th>
+                <th>结果提示</th>
+                <th>单位</th>
+                <th>参考范围</th>
+                <th>备注</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="(item, index) in labDetailList" :key="index">
+                <td>{{ item.itemName }}</td>
+                <td>{{ item.itemAbbr }}</td>
+                <td>{{ item.resultValue }}</td>
+                <td>{{ item.resultHint }}</td>
+                <td>{{ item.resultUnit }}</td>
+                <td>{{ item.refRange }}</td>
+                <td>{{ item.itemRemark }}</td>
+              </tr>
+              <tr v-if="labDetailList.length === 0"><td colspan="7" class="empty-tip">暂无数据</td></tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from 'vue'
+import request from '@/utils/request'
+
+// 查询条件
+const searchForm = reactive({
+  name: '',
+  deptCode: '',
+  wardCode: '',
+  roomNo: '',
+  bedNo: '',
+  inpatientNo: '',
+  nurseLevel: '',
+  inpatientStatus: '',
+  admissionDateStart: '',
+  admissionDateEnd: ''
+})
+
+// 下拉列表数据
+const deptList = ref([])
+const wardList = ref([])
+const roomList = ref([])
+const bedList = ref([])
+const inpatientStatusList = ref([])
+const nurseLevelList = ref([])
+
+// 表格数据
+const tableData = ref([])
+
+// 加载状态
+const loading = ref(false)
+
+// 分页相关
+const currentPage = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const jumpPage = ref(1)
+
+// 计算总页数
+const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1)
+
+// 计算显示的页码
+const displayPages = computed(() => {
+  const pages = []
+  const maxShow = 5
+  let start = Math.max(1, currentPage.value - Math.floor(maxShow / 2))
+  let end = Math.min(totalPages.value, start + maxShow - 1)
+  if (end - start + 1 < maxShow) {
+    start = Math.max(1, end - maxShow + 1)
+  }
+  for (let i = start; i <= end; i++) {
+    pages.push(i)
+  }
+  return pages
+})
+
+// 详情弹窗
+const detailVisible = ref(false)
+const currentPatient = ref({})
+const activeTab = ref('basic')
+const examSubTab = ref('check')
+
+// 各页签数据(暂时空数据,待后端对接)
+const diagnoseList = ref([])
+const orderList = ref([])
+const feeList = ref([])
+const checkList = ref([])
+const labList = ref([])
+
+// 检验报告详情弹窗
+const labDetailVisible = ref(false)
+const currentLab = ref({})
+const labDetailList = ref([])
+
+onMounted(async () => {
+  loading.value = true
+  try {
+    // 使用合并接口,一次请求获取所有数据
+    const res = await request.get('/api/patient/init-data', {
+      params: {
+        page: currentPage.value,
+        pageSize: pageSize.value
+      }
+    })
+    if (res.code === 200 && res.data) {
+      deptList.value = res.data.deptList || []
+      wardList.value = res.data.wardList || []
+      roomList.value = res.data.roomList || []
+      bedList.value = res.data.bedList || []
+      inpatientStatusList.value = res.data.inpatientStatusList || []
+      nurseLevelList.value = res.data.nurseLevelList || []
+      tableData.value = res.data.patientList?.records || res.data.patientList || []
+      total.value = res.data.patientList?.total || tableData.value.length
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+})
+
+// 加载所属科室列表
+const loadDeptList = async () => {
+  try {
+    const res = await request.get('/api/patient/dept-list')
+    if (res.code === 200) {
+      deptList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载科室列表失败:', error)
+  }
+}
+
+// 加载病区列表
+const loadWardList = async () => {
+  try {
+    const params = {}
+    if (searchForm.deptCode) params.deptCode = searchForm.deptCode
+    const res = await request.get('/api/patient/ward-list', { params })
+    if (res.code === 200) {
+      wardList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载病区列表失败:', error)
+  }
+}
+
+// 加载房间号列表
+const loadRoomList = async () => {
+  try {
+    const params = {}
+    if (searchForm.wardCode) params.wardCode = searchForm.wardCode
+    const res = await request.get('/api/patient/room-list', { params })
+    if (res.code === 200) {
+      roomList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载房间号列表失败:', error)
+  }
+}
+
+// 加载床位号列表
+const loadBedList = async () => {
+  try {
+    const params = {}
+    if (searchForm.roomNo) params.roomNo = searchForm.roomNo
+    const res = await request.get('/api/patient/bed-list', { params })
+    if (res.code === 200) {
+      bedList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载床位号列表失败:', error)
+  }
+}
+
+// 加载在院状态列表
+const loadInpatientStatusList = async () => {
+  try {
+    const res = await request.get('/api/patient/inpatient-status-list')
+    if (res.code === 200) {
+      inpatientStatusList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载在院状态列表失败:', error)
+  }
+}
+
+// 加载护理级别列表
+const loadNurseLevelList = async () => {
+  try {
+    const res = await request.get('/api/patient/nurse-level-list')
+    if (res.code === 200) {
+      nurseLevelList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载护理级别列表失败:', error)
+  }
+}
+
+// 科室变化时
+const onDeptChange = () => {
+  searchForm.wardCode = ''
+  searchForm.roomNo = ''
+  searchForm.bedNo = ''
+  loadWardList()
+  roomList.value = []
+  bedList.value = []
+}
+
+// 病区变化时
+const onWardChange = () => {
+  searchForm.roomNo = ''
+  searchForm.bedNo = ''
+  loadRoomList()
+  bedList.value = []
+}
+
+// 房间号变化时
+const onRoomChange = () => {
+  searchForm.bedNo = ''
+  loadBedList()
+}
+
+// 加载数据
+const loadData = async () => {
+  try {
+    const params = {
+      page: currentPage.value,
+      pageSize: pageSize.value
+    }
+    if (searchForm.name) params.name = searchForm.name
+    if (searchForm.deptCode) params.deptCode = searchForm.deptCode
+    if (searchForm.wardCode) params.wardCode = searchForm.wardCode
+    if (searchForm.roomNo) params.roomNo = searchForm.roomNo
+    if (searchForm.bedNo) params.bedNo = searchForm.bedNo
+    if (searchForm.inpatientNo) params.inpatientNo = searchForm.inpatientNo
+    if (searchForm.nurseLevel) params.nurseLevel = searchForm.nurseLevel
+    if (searchForm.inpatientStatus) params.inpatientStatus = searchForm.inpatientStatus
+    if (searchForm.admissionDateStart) params.admissionDateStart = searchForm.admissionDateStart
+    if (searchForm.admissionDateEnd) params.admissionDateEnd = searchForm.admissionDateEnd
+    
+    const res = await request.get('/api/patient/list', { params })
+    if (res.code === 200) {
+      tableData.value = res.data?.records || res.data || []
+      total.value = res.data?.total || tableData.value.length
+    }
+  } catch (error) {
+    console.error('加载数据失败:', error)
+  }
+}
+
+// 切换页码
+const changePage = (page) => {
+  if (page >= 1 && page <= totalPages.value) {
+    currentPage.value = page
+    jumpPage.value = page
+    loading.value = true
+    loadData().finally(() => { loading.value = false })
+  }
+}
+
+// 修改每页条数
+const handlePageSizeChange = () => {
+  currentPage.value = 1
+  loading.value = true
+  loadData().finally(() => { loading.value = false })
+}
+
+// 跳转到指定页
+const handleJumpPage = () => {
+  const page = Math.min(Math.max(1, jumpPage.value), totalPages.value)
+  changePage(page)
+}
+
+// 查询
+const handleSearch = async () => {
+  currentPage.value = 1
+  loading.value = true
+  try {
+    await loadData()
+  } finally {
+    loading.value = false
+  }
+}
+
+// 重置
+const handleReset = async () => {
+  searchForm.name = ''
+  searchForm.deptCode = ''
+  searchForm.wardCode = ''
+  searchForm.roomNo = ''
+  searchForm.bedNo = ''
+  searchForm.inpatientNo = ''
+  searchForm.nurseLevel = ''
+  searchForm.inpatientStatus = ''
+  searchForm.admissionDateStart = ''
+  searchForm.admissionDateEnd = ''
+  currentPage.value = 1
+  loading.value = true
+  try {
+    await Promise.all([loadWardList(), loadRoomList(), loadBedList(), loadData()])
+  } finally {
+    loading.value = false
+  }
+}
+
+// 查看详情
+const showDetail = async (item) => {
+  try {
+    const res = await request.get(`/api/patient/detail/${item.id}`)
+    if (res.code === 200) {
+      currentPatient.value = res.data
+      activeTab.value = 'basic'
+      examSubTab.value = 'check'
+      // 清空各页签数据
+      diagnoseList.value = []
+      orderList.value = []
+      feeList.value = []
+      checkList.value = []
+      labList.value = []
+      detailVisible.value = true
+      // 加载诊断信息(使用患者ID查询)
+      loadDiagnoseList(res.data.id)
+      // 加载医嘱信息(使用患者ID查询)
+      loadOrderList(res.data.id)
+      // 加载费用信息(使用患者ID查询)
+      loadChargeList(res.data.id)
+      // 加载检查报告(使用患者ID查询)
+      loadExamList(res.data.id)
+      // 加载检验报告(使用患者ID查询)
+      loadTestList(res.data.id)
+    }
+  } catch (error) {
+    console.error('获取详情失败:', error)
+  }
+}
+
+// 加载诊断信息列表
+const loadDiagnoseList = async (patientId) => {
+  try {
+    const res = await request.get(`/api/patient/diagnosis/${patientId}`)
+    if (res.code === 200) {
+      diagnoseList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载诊断信息失败:', error)
+  }
+}
+
+// 加载医嘱信息列表
+const loadOrderList = async (patientId) => {
+  try {
+    const res = await request.get(`/api/patient/orders/${patientId}`)
+    if (res.code === 200) {
+      orderList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载医嘱信息失败:', error)
+  }
+}
+
+// 加载费用信息列表
+const loadChargeList = async (patientId) => {
+  try {
+    const res = await request.get(`/api/patient/charges/${patientId}`)
+    if (res.code === 200) {
+      feeList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载费用信息失败:', error)
+  }
+}
+
+// 加载检查报告列表
+const loadExamList = async (patientId) => {
+  try {
+    const res = await request.get(`/api/patient/exams/${patientId}`)
+    if (res.code === 200) {
+      checkList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载检查报告失败:', error)
+  }
+}
+
+// 加载检验报告列表
+const loadTestList = async (patientId) => {
+  try {
+    const res = await request.get(`/api/patient/tests/${patientId}`)
+    if (res.code === 200) {
+      labList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载检验报告失败:', error)
+  }
+}
+
+// 查看检验报告详情
+const showLabDetail = async (item) => {
+  currentLab.value = item
+  labDetailList.value = []
+  labDetailVisible.value = true
+  // 加载检验项目明细
+  try {
+    const res = await request.get(`/api/patient/test-items/${item.id}`)
+    if (res.code === 200) {
+      labDetailList.value = res.data || []
+    }
+  } catch (error) {
+    console.error('加载检验项目明细失败:', error)
+  }
+}
+
+// 格式化日期
+const formatDate = (dateStr) => {
+  if (!dateStr) return ''
+  const date = new Date(dateStr)
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  const hours = String(date.getHours()).padStart(2, '0')
+  const minutes = String(date.getMinutes()).padStart(2, '0')
+  return `${year}-${month}-${day} ${hours}:${minutes}`
+}
+
+// 格式化日期时间
+const formatDateTime = (dateStr) => {
+  if (!dateStr) return ''
+  const date = new Date(dateStr)
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  const hours = String(date.getHours()).padStart(2, '0')
+  const minutes = String(date.getMinutes()).padStart(2, '0')
+  const seconds = String(date.getSeconds()).padStart(2, '0')
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+
+// 格式化诊断状态
+const formatDiagnoseStatus = (status) => {
+  const map = {
+    1: '确诊',
+    2: '疑似',
+    3: '排除',
+    4: '待查'
+  }
+  return map[status] || status || ''
+}
+
+// 格式化护理级别
+const formatNurseLevel = (level) => {
+  const map = {
+    '0': '特级',
+    '1': '一级',
+    '2': '二级',
+    '3': '三级',
+    '-1': '未设置'
+  }
+  return map[String(level)] || level || ''
+}
+
+// 格式化在院状态
+const formatInpatientStatus = (status) => {
+  const map = {
+    '1': '入院',
+    '2': '已入科',
+    '3': '待入科',
+    '4': '已出院',
+    '-1': '未设置'
+  }
+  return map[String(status)] || status || ''
+}
+
+// 格式化医嘱类型
+const formatOrderType = (type) => {
+  const map = {
+    1: '长期医嘱',
+    2: '临时医嘱',
+    3: '出院医嘱'
+  }
+  return map[type] || type || ''
+}
+
+// 格式化执行状态
+const formatExecStatus = (status) => {
+  const map = {
+    1: '未执行',
+    2: '执行中',
+    3: '已执行',
+    4: '已停止'
+  }
+  return map[status] || status || ''
+}
+</script>
+
+<style scoped>
+.page-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+}
+.page-header {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 15px;
+  margin-bottom: 20px;
+  font-size: 14px;
+}
+.breadcrumb-label { color: #666; }
+.breadcrumb-path { color: #666; }
+.breadcrumb-separator { margin: 0 5px; color: #666; }
+.breadcrumb-current { color: #409eff; }
+
+/* 查询栏 */
+.search-bar {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 10px;
+  padding: 12px 15px;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
+.search-item {
+  display: flex;
+  align-items: center;
+}
+.search-item label {
+  margin-right: 6px;
+  color: #666;
+  font-size: 14px;
+  white-space: nowrap;
+}
+.search-item input,
+.search-item select {
+  padding: 6px 8px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 13px;
+}
+.search-item input { width: 100px; }
+.search-item select { width: 100px; }
+.search-item input[type="date"] { width: 120px; }
+.date-sep {
+  margin: 0 3px;
+  color: #666;
+}
+.search-btn {
+  padding: 6px 16px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+  font-size: 13px;
+}
+.search-btn:hover { background: #66b1ff; }
+.reset-btn {
+  padding: 6px 16px;
+  background: #fff;
+  color: #666;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  white-space: nowrap;
+  font-size: 13px;
+}
+.reset-btn:hover { border-color: #409eff; color: #409eff; }
+
+/* 数据表格 */
+.table-container {
+  overflow-x: auto;
+}
+.data-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 14px;
+}
+.data-table th,
+.data-table td {
+  padding: 12px;
+  text-align: left;
+  border-bottom: 1px solid #eee;
+  white-space: nowrap;
+}
+.data-table th {
+  background: #f5f7fa;
+  font-weight: 500;
+  color: #333;
+}
+.data-table tbody tr:hover {
+  background: #f5f7fa;
+}
+.empty-tip {
+  text-align: center;
+  color: #999;
+  padding: 40px 0;
+}
+.detail-btn {
+  padding: 4px 12px;
+  background: #409eff;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+}
+.detail-btn:hover { background: #66b1ff; }
+
+/* 加载状态 */
+.loading-mask {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 0;
+  color: #909399;
+}
+.loading-spinner {
+  width: 32px;
+  height: 32px;
+  border: 3px solid #f3f3f3;
+  border-top: 3px solid #409eff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 12px;
+}
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+/* 弹窗样式 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+.modal-content {
+  background: #fff;
+  border-radius: 8px;
+  max-width: 90%;
+  max-height: 90%;
+  overflow: hidden;
+}
+.detail-modal {
+  width: 1100px;
+  height: 600px;
+}
+.detail-modal .modal-body {
+  height: calc(600px - 60px);
+  overflow-y: auto;
+}
+/* 检验报告详情弹窗 */
+.lab-detail-modal {
+  width: 850px;
+  height: 450px;
+}
+.lab-detail-modal .modal-body {
+  height: calc(450px - 60px);
+  overflow-y: auto;
+}
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 20px;
+  border-bottom: 1px solid #eee;
+  font-size: 16px;
+  font-weight: 500;
+}
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 20px;
+  cursor: pointer;
+  color: #999;
+}
+.close-btn:hover { color: #333; }
+.modal-body {
+  padding: 20px;
+  max-height: 70vh;
+  overflow-y: auto;
+}
+
+/* 详情区域 */
+.detail-section {
+  margin-bottom: 20px;
+}
+.detail-section h4 {
+  margin: 0 0 15px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #eee;
+  color: #333;
+}
+
+/* 信息分组 */
+.info-group {
+  margin-bottom: 20px;
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  overflow: hidden;
+}
+.group-title {
+  background: #f5f7fa;
+  padding: 10px 15px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  border-bottom: 1px solid #e4e7ed;
+}
+.info-group .detail-grid {
+  padding: 15px;
+}
+
+.detail-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 12px;
+}
+.detail-item {
+  display: flex;
+  font-size: 14px;
+}
+.detail-item .label {
+  color: #666;
+  white-space: nowrap;
+}
+.detail-item .value {
+  color: #333;
+}
+.detail-item.full-width {
+  grid-column: span 4;
+}
+
+/* 页签样式 */
+.tab-bar {
+  display: flex;
+  border-bottom: 2px solid #e4e7ed;
+  margin-bottom: 15px;
+}
+.tab-item {
+  padding: 10px 20px;
+  cursor: pointer;
+  color: #666;
+  font-size: 14px;
+  border-bottom: 2px solid transparent;
+  margin-bottom: -2px;
+  transition: all 0.3s;
+}
+.tab-item:hover {
+  color: #409eff;
+}
+.tab-item.active {
+  color: #409eff;
+  border-bottom-color: #409eff;
+}
+
+/* 子页签样式 */
+.sub-tab-bar {
+  display: flex;
+  margin-bottom: 15px;
+  gap: 10px;
+}
+.sub-tab-item {
+  padding: 6px 16px;
+  cursor: pointer;
+  color: #666;
+  font-size: 13px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  transition: all 0.3s;
+}
+.sub-tab-item:hover {
+  background: #e4e7ed;
+}
+.sub-tab-item.active {
+  background: #409eff;
+  color: #fff;
+}
+
+/* 页签内容 */
+.tab-content {
+  min-height: 400px;
+  max-height: 450px;
+  overflow-y: auto;
+}
+
+/* 信息表格 */
+.info-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 13px;
+}
+.info-table th,
+.info-table td {
+  padding: 10px 8px;
+  text-align: left;
+  border: 1px solid #ebeef5;
+  white-space: nowrap;
+}
+.info-table th {
+  background: #f5f7fa;
+  font-weight: 500;
+  color: #333;
+}
+.info-table tbody tr:hover {
+  background: #f5f7fa;
+}
+
+/* 分页样式 */
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  padding: 15px 0;
+  border-top: 1px solid #eee;
+}
+.pagination-info {
+  color: #666;
+  font-size: 14px;
+}
+.pagination-controls {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.page-btn {
+  padding: 6px 12px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-btn:hover:not(:disabled) {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-btn:disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+.page-numbers {
+  display: flex;
+  gap: 5px;
+}
+.page-num {
+  min-width: 32px;
+  height: 28px;
+  padding: 0 6px;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+}
+.page-num:hover {
+  color: #409eff;
+  border-color: #c6e2ff;
+}
+.page-num.active {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+}
+.page-size-selector select {
+  padding: 5px 10px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  font-size: 13px;
+  margin-left: 10px;
+}
+.page-jump {
+  font-size: 13px;
+  color: #606266;
+  margin-left: 10px;
+}
+.page-jump input {
+  width: 50px;
+  padding: 5px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  text-align: center;
+  margin: 0 5px;
+}
+</style>

+ 22 - 0
frontend/vite.config.js

@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { resolve } from 'path'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src')
+    }
+  },
+  server: {
+    port: 3000,
+    proxy: {
+      '/shixian': {
+        target: 'http://localhost:8080',
+        changeOrigin: true
+      }
+    }
+  }
+})

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "bishe",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}

+ 102 - 0
pom.xml

@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <!-- Spring Boot 2.7 Parent -->
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.18</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>org.example</groupId>
+    <artifactId>bishe</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!-- Spring Boot Web -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- MyBatis Spring Boot Starter -->
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.3.2</version>
+        </dependency>
+
+        <!-- MySQL Driver -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>8.0.33</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- Lombok (可选,简化代码) -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Spring Boot Test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- Redis 缓存 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        
+        <!-- 连接池 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <includes>
+                    <include>**/*.properties</include>
+                    <include>**/*.xml</include>
+                </includes>
+            </resource>
+        </resources>
+    </build>
+
+</project>

+ 13 - 0
src/main/java/org/example/Main.java

@@ -0,0 +1,13 @@
+package org.example;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@MapperScan("org.example.mapper")
+public class Main {
+    public static void main(String[] args) {
+        SpringApplication.run(Main.class, args);
+    }
+}

+ 47 - 0
src/main/java/org/example/config/CacheConstants.java

@@ -0,0 +1,47 @@
+package org.example.config;
+
+/**
+ * 缓存名称常量
+ */
+public class CacheConstants {
+    
+    /**
+     * 医院信息缓存
+     */
+    public static final String CACHE_HOSPITAL = "hospital";
+    
+    /**
+     * 科室缓存
+     */
+    public static final String CACHE_DEPARTMENT = "department";
+    
+    /**
+     * 病区缓存
+     */
+    public static final String CACHE_WARD_AREA = "wardArea";
+    
+    /**
+     * 病房缓存
+     */
+    public static final String CACHE_ROOM = "room";
+    
+    /**
+     * 床位缓存
+     */
+    public static final String CACHE_BED = "bed";
+    
+    /**
+     * 患者缓存
+     */
+    public static final String CACHE_PATIENT = "patient";
+    
+    /**
+     * 设备缓存
+     */
+    public static final String CACHE_TERMINAL = "terminal";
+    
+    /**
+     * 下拉选项缓存(科室列表、病区列表等)
+     */
+    public static final String CACHE_OPTIONS = "options";
+}

+ 32 - 0
src/main/java/org/example/config/CorsConfig.java

@@ -0,0 +1,32 @@
+package org.example.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+/**
+ * 跨域配置
+ */
+@Configuration
+public class CorsConfig {
+
+    @Bean
+    public CorsFilter corsFilter() {
+        CorsConfiguration config = new CorsConfiguration();
+        // 允许所有域名访问
+        config.addAllowedOriginPattern("*");
+        // 允许携带cookie
+        config.setAllowCredentials(true);
+        // 允许所有请求方法
+        config.addAllowedMethod("*");
+        // 允许所有请求头
+        config.addAllowedHeader("*");
+        
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", config);
+        
+        return new CorsFilter(source);
+    }
+}

+ 71 - 0
src/main/java/org/example/config/RedisConfig.java

@@ -0,0 +1,71 @@
+package org.example.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.time.Duration;
+
+/**
+ * Redis配置类
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig {
+
+    /**
+     * 配置RedisTemplate
+     */
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        StringRedisSerializer stringSerializer = new StringRedisSerializer();
+        template.setKeySerializer(stringSerializer);
+        template.setHashKeySerializer(stringSerializer);
+
+        // 使用GenericJackson2JsonRedisSerializer来序列化和反序列化redis的value值
+        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
+        template.setValueSerializer(jsonSerializer);
+        template.setHashValueSerializer(jsonSerializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    /**
+     * 配置缓存管理器
+     */
+    @Bean
+    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
+        // 配置序列化
+        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
+                // 设置缓存过期时间为30分钟
+                .entryTtl(Duration.ofMinutes(30))
+                // 设置key的序列化方式
+                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
+                // 设置value的序列化方式
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
+                // 不缓存null值
+                .disableCachingNullValues();
+
+        return RedisCacheManager.builder(connectionFactory)
+                .cacheDefaults(config)
+                .build();
+    }
+}

+ 23 - 0
src/main/java/org/example/config/WebConfig.java

@@ -0,0 +1,23 @@
+package org.example.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 静态资源配置
+ */
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Value("${file.upload-path:uploads/}")
+    private String uploadPath;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        // 配置上传文件的访问路径
+        registry.addResourceHandler("/uploads/**")
+                .addResourceLocations("file:" + uploadPath);
+    }
+}

+ 22 - 0
src/main/java/org/example/controller/BaseController.java

@@ -0,0 +1,22 @@
+package org.example.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 基础控制器示例
+ */
+@RestController
+public class BaseController {
+
+    @GetMapping("/hello")
+    public Map<String, Object> hello() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("code", 200);
+        result.put("message", "Spring Boot + MyBatis 启动成功!");
+        return result;
+    }
+}

+ 185 - 0
src/main/java/org/example/controller/BedController.java

@@ -0,0 +1,185 @@
+package org.example.controller;
+
+import org.example.entity.Bed;
+import org.example.entity.PageResult;
+import org.example.service.BedService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 床位控制器
+ */
+@RestController
+@RequestMapping("/api/bed")
+public class BedController {
+
+    @Autowired
+    private BedService bedService;
+
+    /**
+     * 查询床位列表(支持分页)
+     */
+    @GetMapping("/list")
+    public Map<String, Object> list(
+            @RequestParam(required = false) String code,
+            @RequestParam(required = false) String belongDept,
+            @RequestParam(required = false) String belongWard,
+            @RequestParam(required = false) String belongRoom,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        List<Bed> list = bedService.findList(code, belongDept, belongWard, belongRoom);
+        // 分页处理
+        PageResult<Bed> pageResult = PageResult.of(list, page, pageSize);
+        result.put("code", 200);
+        result.put("data", pageResult);
+        return result;
+    }
+
+    /**
+     * 获取所属科室列表(用于下拉框)
+     */
+    @GetMapping("/dept-list")
+    public Map<String, Object> deptList() {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = bedService.findAllBelongDept();
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 根据科室获取病区列表(用于联动下拉框)
+     */
+    @GetMapping("/ward-list")
+    public Map<String, Object> wardList(@RequestParam(required = false) String belongDept) {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = bedService.findWardsByDept(belongDept);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 根据病区获取病房列表(用于联动下拉框)
+     */
+    @GetMapping("/room-list")
+    public Map<String, Object> roomList(@RequestParam(required = false) String belongWard) {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = bedService.findRoomsByWard(belongWard);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 获取床位详情
+     */
+    @GetMapping("/detail/{id}")
+    public Map<String, Object> detail(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        Bed bed = bedService.findById(id);
+        if (bed != null) {
+            result.put("code", 200);
+            result.put("data", bed);
+        } else {
+            result.put("code", 404);
+            result.put("message", "床位不存在");
+        }
+        return result;
+    }
+
+    /**
+     * 保存床位
+     */
+    @PostMapping("/save")
+    public Map<String, Object> save(@RequestBody Bed bed) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            bedService.save(bed);
+            result.put("code", 200);
+            result.put("message", "保存成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "保存失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 批量设置床位代码
+     */
+    @PostMapping("/batch-set-code")
+    public Map<String, Object> batchSetCode(@RequestBody Map<String, Object> params,
+                                            @RequestHeader(value = "X-User-Type", required = false) String userType) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 权限校验:只有管理员才能设置床位代码
+        if (!"admin".equals(userType)) {
+            result.put("code", 403);
+            result.put("message", "无权操作,只有管理员才能设置床位代码");
+            return result;
+        }
+        
+        try {
+            @SuppressWarnings("unchecked")
+            List<?> idList = (List<?>) params.get("ids");
+            @SuppressWarnings("unchecked")
+            List<String> codes = (List<String>) params.get("codes");
+            
+            if (idList == null || codes == null || idList.size() != codes.size()) {
+                result.put("code", 400);
+                result.put("message", "参数错误");
+                return result;
+            }
+            
+            // 将id转换为Long类型
+            List<Long> ids = new ArrayList<>();
+            for (Object id : idList) {
+                if (id instanceof Integer) {
+                    ids.add(((Integer) id).longValue());
+                } else if (id instanceof Long) {
+                    ids.add((Long) id);
+                } else {
+                    ids.add(Long.parseLong(id.toString()));
+                }
+            }
+            
+            List<String> errors = bedService.batchSetCode(ids, codes);
+            if (errors.isEmpty()) {
+                result.put("code", 200);
+                result.put("message", "设置成功");
+            } else {
+                result.put("code", 200);
+                result.put("message", "部分设置成功");
+                result.put("errors", errors);
+            }
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "设置失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 删除床位
+     */
+    @DeleteMapping("/delete/{id}")
+    public Map<String, Object> delete(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            bedService.deleteById(id);
+            result.put("code", 200);
+            result.put("message", "删除成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "删除失败:" + e.getMessage());
+        }
+        return result;
+    }
+}

+ 91 - 0
src/main/java/org/example/controller/DepartmentController.java

@@ -0,0 +1,91 @@
+package org.example.controller;
+
+import org.example.entity.Department;
+import org.example.entity.PageResult;
+import org.example.service.DepartmentService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 科室控制器
+ */
+@RestController
+@RequestMapping("/api/department")
+public class DepartmentController {
+
+    @Autowired
+    private DepartmentService departmentService;
+
+    /**
+     * 查询科室列表(支持分页)
+     */
+    @GetMapping("/list")
+    public Map<String, Object> list(
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) Integer enabled,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        List<Department> list = departmentService.findList(name, enabled);
+        // 分页处理
+        PageResult<Department> pageResult = PageResult.of(list, page, pageSize);
+        result.put("code", 200);
+        result.put("data", pageResult);
+        return result;
+    }
+
+    /**
+     * 获取科室详情
+     */
+    @GetMapping("/detail/{id}")
+    public Map<String, Object> detail(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        Department department = departmentService.findById(id);
+        if (department != null) {
+            result.put("code", 200);
+            result.put("data", department);
+        } else {
+            result.put("code", 404);
+            result.put("message", "科室不存在");
+        }
+        return result;
+    }
+
+    /**
+     * 保存科室
+     */
+    @PostMapping("/save")
+    public Map<String, Object> save(@RequestBody Department department) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            departmentService.save(department);
+            result.put("code", 200);
+            result.put("message", "保存成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "保存失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 删除科室
+     */
+    @DeleteMapping("/delete/{id}")
+    public Map<String, Object> delete(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            departmentService.deleteById(id);
+            result.put("code", 200);
+            result.put("message", "删除成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "删除失败:" + e.getMessage());
+        }
+        return result;
+    }
+}

+ 110 - 0
src/main/java/org/example/controller/HospitalController.java

@@ -0,0 +1,110 @@
+package org.example.controller;
+
+import org.example.entity.Hospital;
+import org.example.service.HospitalService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 医院信息控制器
+ */
+@RestController
+@RequestMapping("/api/hospital")
+public class HospitalController {
+
+    @Autowired
+    private HospitalService hospitalService;
+
+    @Value("${file.upload-path:uploads/}")
+    private String uploadPath;
+
+    /**
+     * 获取医院信息
+     */
+    @GetMapping("/info")
+    public Map<String, Object> getHospital() {
+        Map<String, Object> result = new HashMap<>();
+        Hospital hospital = hospitalService.getHospital();
+        result.put("code", 200);
+        result.put("data", hospital);
+        return result;
+    }
+
+    /**
+     * 保存医院信息
+     */
+    @PostMapping("/save")
+    public Map<String, Object> saveHospital(@RequestBody Hospital hospital) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            hospitalService.saveHospital(hospital);
+            result.put("code", 200);
+            result.put("message", "保存成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "保存失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 上传Logo
+     */
+    @PostMapping("/upload-logo")
+    public Map<String, Object> uploadLogo(@RequestParam("file") MultipartFile file) {
+        Map<String, Object> result = new HashMap<>();
+        
+        if (file.isEmpty()) {
+            result.put("code", 500);
+            result.put("message", "请选择文件");
+            return result;
+        }
+
+        // 检查文件大小(10MB)
+        if (file.getSize() > 10 * 1024 * 1024) {
+            result.put("code", 500);
+            result.put("message", "文件大小不能超过10M");
+            return result;
+        }
+
+        // 检查文件类型
+        String originalFilename = file.getOriginalFilename();
+        String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
+        if (!".jpg".equals(suffix) && !".png".equals(suffix) && !".jpeg".equals(suffix)) {
+            result.put("code", 500);
+            result.put("message", "只支持jpg、png格式图片");
+            return result;
+        }
+
+        try {
+            // 创建上传目录
+            File uploadDir = new File(uploadPath);
+            if (!uploadDir.exists()) {
+                uploadDir.mkdirs();
+            }
+
+            // 生成新文件名
+            String newFileName = UUID.randomUUID().toString() + suffix;
+            File destFile = new File(uploadPath + newFileName);
+            file.transferTo(destFile);
+
+            result.put("code", 200);
+            result.put("message", "上传成功");
+            // 返回前端可直接访问的路径
+            result.put("data", "/img/" + newFileName);
+        } catch (IOException e) {
+            result.put("code", 500);
+            result.put("message", "上传失败:" + e.getMessage());
+        }
+
+        return result;
+    }
+}

+ 38 - 0
src/main/java/org/example/controller/LoginController.java

@@ -0,0 +1,38 @@
+package org.example.controller;
+
+import org.example.service.LoginService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 登录控制器
+ */
+@RestController
+@RequestMapping("/api")
+public class LoginController {
+
+    @Autowired
+    private LoginService loginService;
+
+    /**
+     * 用户登录
+     */
+    @PostMapping("/login/user")
+    public Map<String, Object> userLogin(@RequestBody Map<String, String> params) {
+        String username = params.get("username");
+        String password = params.get("password");
+        return loginService.userLogin(username, password);
+    }
+
+    /**
+     * 管理员登录
+     */
+    @PostMapping("/login/admin")
+    public Map<String, Object> adminLogin(@RequestBody Map<String, String> params) {
+        String username = params.get("username");
+        String password = params.get("password");
+        return loginService.adminLogin(username, password);
+    }
+}

+ 336 - 0
src/main/java/org/example/controller/PatientController.java

@@ -0,0 +1,336 @@
+package org.example.controller;
+
+import org.example.entity.PageResult;
+import org.example.entity.Patient;
+import org.example.entity.PatientDiagnosis;
+import org.example.entity.PatientOrder;
+import org.example.entity.PatientCharge;
+import org.example.entity.PatientExam;
+import org.example.entity.PatientTest;
+import org.example.entity.PatientTestItem;
+import org.example.service.PatientService;
+import org.example.service.PatientDiagnosisService;
+import org.example.service.PatientOrderService;
+import org.example.service.PatientChargeService;
+import org.example.service.PatientExamService;
+import org.example.service.PatientTestService;
+import org.example.service.PatientTestItemService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 患者控制器
+ */
+@RestController
+@RequestMapping("/api/patient")
+public class PatientController {
+
+    @Autowired
+    private PatientService patientService;
+
+    @Autowired
+    private PatientDiagnosisService patientDiagnosisService;
+
+    @Autowired
+    private PatientOrderService patientOrderService;
+
+    @Autowired
+    private PatientChargeService patientChargeService;
+
+    @Autowired
+    private PatientExamService patientExamService;
+
+    @Autowired
+    private PatientTestService patientTestService;
+
+    @Autowired
+    private PatientTestItemService patientTestItemService;
+
+    /**
+     * 获取页面初始化数据(合并接口,减少请求次数,支持分页)
+     */
+    @GetMapping("/init-data")
+    public Map<String, Object> initData(
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("code", 200);
+        result.put("data", patientService.getInitData(page, pageSize));
+        return result;
+    }
+
+    /**
+     * 查询患者列表(支持分页)
+     */
+    @GetMapping("/list")
+    public Map<String, Object> list(
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) String deptCode,
+            @RequestParam(required = false) String wardCode,
+            @RequestParam(required = false) String roomNo,
+            @RequestParam(required = false) String bedNo,
+            @RequestParam(required = false) String inpatientNo,
+            @RequestParam(required = false) String nurseLevel,
+            @RequestParam(required = false) String inpatientStatus,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date admissionDateStart,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date admissionDateEnd,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        List<Patient> list = patientService.findList(name, deptCode, wardCode, roomNo, bedNo,
+                inpatientNo, nurseLevel, inpatientStatus, admissionDateStart, admissionDateEnd);
+        // 分页处理
+        PageResult<Patient> pageResult = PageResult.of(list, page, pageSize);
+        result.put("code", 200);
+        result.put("data", pageResult);
+        return result;
+    }
+
+    /**
+     * 获取所属科室列表(用于下拉框)
+     */
+    @GetMapping("/dept-list")
+    public Map<String, Object> deptList() {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = patientService.findAllDeptCode();
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 根据科室获取病区列表(用于联动下拉框)
+     */
+    @GetMapping("/ward-list")
+    public Map<String, Object> wardList(@RequestParam(required = false) String deptCode) {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = patientService.findWardsByDept(deptCode);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 根据病区获取房间号列表(用于联动下拉框)
+     */
+    @GetMapping("/room-list")
+    public Map<String, Object> roomList(@RequestParam(required = false) String wardCode) {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = patientService.findRoomsByWard(wardCode);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 根据房间号获取床位号列表(用于联动下拉框)
+     */
+    @GetMapping("/bed-list")
+    public Map<String, Object> bedList(@RequestParam(required = false) String roomNo) {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = patientService.findBedsByRoom(roomNo);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 获取在院状态列表(用于下拉框)
+     */
+    @GetMapping("/inpatient-status-list")
+    public Map<String, Object> inpatientStatusList() {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = patientService.findAllInpatientStatus();
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 获取护理级别列表(用于下拉框)
+     */
+    @GetMapping("/nurse-level-list")
+    public Map<String, Object> nurseLevelList() {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = patientService.findAllNurseLevel();
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 获取患者详情
+     */
+    @GetMapping("/detail/{id}")
+    public Map<String, Object> detail(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        Patient patient = patientService.findById(id);
+        if (patient != null) {
+            result.put("code", 200);
+            result.put("data", patient);
+        } else {
+            result.put("code", 404);
+            result.put("message", "患者不存在");
+        }
+        return result;
+    }
+
+    /**
+     * 保存患者
+     */
+    @PostMapping("/save")
+    public Map<String, Object> save(@RequestBody Patient patient) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            patientService.save(patient);
+            result.put("code", 200);
+            result.put("message", "保存成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "保存失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 删除患者
+     */
+    @DeleteMapping("/delete/{id}")
+    public Map<String, Object> delete(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            patientService.deleteById(id);
+            result.put("code", 200);
+            result.put("message", "删除成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "删除失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取患者诊断信息列表(通过患者ID查询)
+     */
+    @GetMapping("/diagnosis/{patientId}")
+    public Map<String, Object> getDiagnosisList(@PathVariable Long patientId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<PatientDiagnosis> list = patientDiagnosisService.findByPatientId(patientId);
+            result.put("code", 200);
+            result.put("data", list);
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "获取诊断信息失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取患者诊断信息列表(通过身份证号查询)
+     */
+    @GetMapping("/diagnosis/idcard/{idCard}")
+    public Map<String, Object> getDiagnosisListByIdCard(@PathVariable String idCard) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<PatientDiagnosis> list = patientDiagnosisService.findByIdCard(idCard);
+            result.put("code", 200);
+            result.put("data", list);
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "获取诊断信息失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取患者医嘱信息列表(通过患者ID查询)
+     */
+    @GetMapping("/orders/{patientId}")
+    public Map<String, Object> getOrderList(@PathVariable Long patientId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<PatientOrder> list = patientOrderService.findByPatientId(patientId);
+            result.put("code", 200);
+            result.put("data", list);
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "获取医嘱信息失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取患者费用信息列表(通过患者ID查询)
+     */
+    @GetMapping("/charges/{patientId}")
+    public Map<String, Object> getChargeList(@PathVariable Long patientId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<PatientCharge> list = patientChargeService.findByPatientId(patientId);
+            result.put("code", 200);
+            result.put("data", list);
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "获取费用信息失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取患者检查报告列表(通过患者ID查询)
+     */
+    @GetMapping("/exams/{patientId}")
+    public Map<String, Object> getExamList(@PathVariable Long patientId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<PatientExam> list = patientExamService.findByPatientId(patientId);
+            result.put("code", 200);
+            result.put("data", list);
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "获取检查报告失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取患者检验报告列表(通过患者表ID查询)
+     */
+    @GetMapping("/tests/{patientId}")
+    public Map<String, Object> getTestList(@PathVariable Long patientId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<PatientTest> list = patientTestService.findByPatientTableId(patientId);
+            result.put("code", 200);
+            result.put("data", list);
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "获取检验报告失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取检验报告项目明细列表(通过检验报告ID查询)
+     */
+    @GetMapping("/test-items/{testId}")
+    public Map<String, Object> getTestItemList(@PathVariable Long testId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<PatientTestItem> list = patientTestItemService.findByTestId(testId);
+            result.put("code", 200);
+            result.put("data", list);
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "获取检验项目明细失败:" + e.getMessage());
+        }
+        return result;
+    }
+}

+ 173 - 0
src/main/java/org/example/controller/RoomController.java

@@ -0,0 +1,173 @@
+package org.example.controller;
+
+import org.example.entity.PageResult;
+import org.example.entity.Room;
+import org.example.service.RoomService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 病房控制器
+ */
+@RestController
+@RequestMapping("/api/room")
+public class RoomController {
+
+    @Autowired
+    private RoomService roomService;
+
+    /**
+     * 查询病房列表(支持分页)
+     */
+    @GetMapping("/list")
+    public Map<String, Object> list(
+            @RequestParam(required = false) String code,
+            @RequestParam(required = false) String belongDept,
+            @RequestParam(required = false) String belongWard,
+            @RequestParam(required = false) Integer enabled,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        List<Room> list = roomService.findList(code, belongDept, belongWard, enabled);
+        // 分页处理
+        PageResult<Room> pageResult = PageResult.of(list, page, pageSize);
+        result.put("code", 200);
+        result.put("data", pageResult);
+        return result;
+    }
+
+    /**
+     * 获取所属科室列表(用于下拉框)
+     */
+    @GetMapping("/dept-list")
+    public Map<String, Object> deptList() {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = roomService.findAllBelongDept();
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 根据科室获取病区列表(用于联动下拉框)
+     */
+    @GetMapping("/ward-list")
+    public Map<String, Object> wardList(@RequestParam(required = false) String belongDept) {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = roomService.findWardsByDept(belongDept);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 获取病房详情
+     */
+    @GetMapping("/detail/{id}")
+    public Map<String, Object> detail(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        Room room = roomService.findById(id);
+        if (room != null) {
+            result.put("code", 200);
+            result.put("data", room);
+        } else {
+            result.put("code", 404);
+            result.put("message", "病房不存在");
+        }
+        return result;
+    }
+
+    /**
+     * 保存病房
+     */
+    @PostMapping("/save")
+    public Map<String, Object> save(@RequestBody Room room) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            roomService.save(room);
+            result.put("code", 200);
+            result.put("message", "保存成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "保存失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 批量设置病房代码
+     */
+    @PostMapping("/batch-set-code")
+    public Map<String, Object> batchSetCode(@RequestBody Map<String, Object> params,
+                                            @RequestHeader(value = "X-User-Type", required = false) String userType) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 权限校验:只有管理员才能设置病房代码
+        if (!"admin".equals(userType)) {
+            result.put("code", 403);
+            result.put("message", "无权操作,只有管理员才能设置病房代码");
+            return result;
+        }
+        
+        try {
+            @SuppressWarnings("unchecked")
+            List<?> idList = (List<?>) params.get("ids");
+            @SuppressWarnings("unchecked")
+            List<String> codes = (List<String>) params.get("codes");
+            
+            if (idList == null || codes == null || idList.size() != codes.size()) {
+                result.put("code", 400);
+                result.put("message", "参数错误");
+                return result;
+            }
+            
+            // 将id转换为Long类型
+            List<Long> ids = new ArrayList<>();
+            for (Object id : idList) {
+                if (id instanceof Integer) {
+                    ids.add(((Integer) id).longValue());
+                } else if (id instanceof Long) {
+                    ids.add((Long) id);
+                } else {
+                    ids.add(Long.parseLong(id.toString()));
+                }
+            }
+            
+            List<String> errors = roomService.batchSetCode(ids, codes);
+            if (errors.isEmpty()) {
+                result.put("code", 200);
+                result.put("message", "设置成功");
+            } else {
+                result.put("code", 200);
+                result.put("message", "部分设置成功");
+                result.put("errors", errors);
+            }
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "设置失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 删除病房
+     */
+    @DeleteMapping("/delete/{id}")
+    public Map<String, Object> delete(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            roomService.deleteById(id);
+            result.put("code", 200);
+            result.put("message", "删除成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "删除失败:" + e.getMessage());
+        }
+        return result;
+    }
+}

+ 181 - 0
src/main/java/org/example/controller/TerminalController.java

@@ -0,0 +1,181 @@
+package org.example.controller;
+
+import org.example.entity.PageResult;
+import org.example.entity.Terminal;
+import org.example.entity.Department;
+import org.example.entity.WardArea;
+import org.example.service.TerminalService;
+import org.example.service.DepartmentService;
+import org.example.service.WardAreaService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备控制器
+ */
+@RestController
+@RequestMapping("/api/terminal")
+public class TerminalController {
+
+    @Autowired
+    private TerminalService terminalService;
+    
+    @Autowired
+    private DepartmentService departmentService;
+    
+    @Autowired
+    private WardAreaService wardAreaService;
+
+    /**
+     * 获取页面初始化数据(合并接口,支持分页)
+     */
+    @GetMapping("/init-data")
+    public Map<String, Object> initData(
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("code", 200);
+        result.put("data", terminalService.getInitData(page, pageSize));
+        return result;
+    }
+
+    /**
+     * 查询设备列表(支持分页)
+     */
+    @GetMapping("/list")
+    public Map<String, Object> list(
+            @RequestParam(required = false) String terminalType,
+            @RequestParam(required = false) String deptCode,
+            @RequestParam(required = false) String wardCode,
+            @RequestParam(required = false) Integer isOnline,
+            @RequestParam(required = false) String terminalDesc,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        List<Terminal> list = terminalService.findList(terminalType, deptCode, wardCode, isOnline, terminalDesc);
+        // 分页处理
+        PageResult<Terminal> pageResult = PageResult.of(list, page, pageSize);
+        result.put("code", 200);
+        result.put("data", pageResult);
+        return result;
+    }
+
+    /**
+     * 获取设备类型列表(用于下拉框)
+     */
+    @GetMapping("/type-list")
+    public Map<String, Object> typeList() {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = terminalService.findAllTerminalTypes();
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 获取所属科室列表(用于下拉框)
+     */
+    @GetMapping("/dept-list")
+    public Map<String, Object> deptList() {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = terminalService.findAllDeptCode();
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 根据科室获取病区列表(用于联动下拉框)
+     */
+    @GetMapping("/ward-list")
+    public Map<String, Object> wardList(@RequestParam(required = false) String deptCode) {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = terminalService.findWardsByDept(deptCode);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 获取设备详情
+     */
+    @GetMapping("/detail/{id}")
+    public Map<String, Object> detail(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        Terminal terminal = terminalService.findById(id);
+        if (terminal != null) {
+            result.put("code", 200);
+            result.put("data", terminal);
+        } else {
+            result.put("code", 404);
+            result.put("message", "设备不存在");
+        }
+        return result;
+    }
+    
+    /**
+     * 新增设备
+     */
+    @PostMapping("/add")
+    public Map<String, Object> add(@RequestBody Terminal terminal,
+                                   @RequestHeader(value = "X-User-Type", required = false) String userType) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 权限校验:只有管理员才能新增设备
+        if (!"admin".equals(userType)) {
+            result.put("code", 403);
+            result.put("message", "无权操作,只有管理员才能新增设备");
+            return result;
+        }
+        
+        String error = terminalService.addTerminal(terminal);
+        if (error == null) {
+            result.put("code", 200);
+            result.put("message", "新增成功");
+        } else {
+            result.put("code", 400);
+            result.put("message", error);
+        }
+        return result;
+    }
+    
+    /**
+     * 获取科室列表(真实数据)
+     */
+    @GetMapping("/department-list")
+    public Map<String, Object> departmentList() {
+        Map<String, Object> result = new HashMap<>();
+        List<Department> list = departmentService.findList(null, 1);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+    
+    /**
+     * 根据科室获取病区列表(真实数据)
+     */
+    @GetMapping("/ward-list-by-dept")
+    public Map<String, Object> wardListByDept(@RequestParam(required = false) String deptName) {
+        Map<String, Object> result = new HashMap<>();
+        List<WardArea> list = wardAreaService.findList(null, deptName, 1);
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+    
+    /**
+     * 检查IP地址是否存在
+     */
+    @GetMapping("/check-ip")
+    public Map<String, Object> checkIp(@RequestParam String ipAddress) {
+        Map<String, Object> result = new HashMap<>();
+        boolean exists = terminalService.checkIpExists(ipAddress);
+        result.put("code", 200);
+        result.put("exists", exists);
+        return result;
+    }
+}

+ 172 - 0
src/main/java/org/example/controller/WardAreaController.java

@@ -0,0 +1,172 @@
+package org.example.controller;
+
+import org.example.entity.PageResult;
+import org.example.entity.WardArea;
+import org.example.service.WardAreaService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 病区控制器
+ */
+@RestController
+@RequestMapping("/api/wardarea")
+public class WardAreaController {
+
+    @Autowired
+    private WardAreaService wardAreaService;
+
+    /**
+     * 查询病区列表(支持分页)
+     */
+    @GetMapping("/list")
+    public Map<String, Object> list(
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) String glkeshi,
+            @RequestParam(required = false) Integer enabled,
+            @RequestParam(required = false, defaultValue = "1") Integer page,
+            @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        List<WardArea> list = wardAreaService.findList(name, glkeshi, enabled);
+        // 分页处理
+        PageResult<WardArea> pageResult = PageResult.of(list, page, pageSize);
+        result.put("code", 200);
+        result.put("data", pageResult);
+        return result;
+    }
+    
+    /**
+     * 获取所有关联科室列表(用于下拉框)
+     */
+    @GetMapping("/glkeshi-list")
+    public Map<String, Object> glkeshiList() {
+        Map<String, Object> result = new HashMap<>();
+        List<String> list = wardAreaService.findAllGlkeshi();
+        result.put("code", 200);
+        result.put("data", list);
+        return result;
+    }
+
+    /**
+     * 获取病区详情
+     */
+    @GetMapping("/detail/{id}")
+    public Map<String, Object> detail(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        WardArea wardArea = wardAreaService.findById(id);
+        if (wardArea != null) {
+            result.put("code", 200);
+            result.put("data", wardArea);
+        } else {
+            result.put("code", 404);
+            result.put("message", "病区不存在");
+        }
+        return result;
+    }
+
+    /**
+     * 保存病区
+     */
+    @PostMapping("/save")
+    public Map<String, Object> save(@RequestBody WardArea wardArea) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            wardAreaService.save(wardArea);
+            result.put("code", 200);
+            result.put("message", "保存成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "保存失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 批量设置病区代码
+     */
+    @PostMapping("/batch-set-code")
+    public Map<String, Object> batchSetCode(@RequestBody Map<String, Object> params,
+                                            @RequestHeader(value = "X-User-Type", required = false) String userType) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 权限校验:只有管理员才能设置病区代码
+        if (!"admin".equals(userType)) {
+            result.put("code", 403);
+            result.put("message", "无权操作,只有管理员才能设置病区代码");
+            return result;
+        }
+        
+        try {
+            @SuppressWarnings("unchecked")
+            List<?> idList = (List<?>) params.get("ids");
+            @SuppressWarnings("unchecked")
+            List<String> codes = (List<String>) params.get("codes");
+            
+            if (idList == null || codes == null || idList.size() != codes.size()) {
+                result.put("code", 400);
+                result.put("message", "参数错误");
+                return result;
+            }
+            
+            // 将id转换为Long类型
+            List<Long> ids = new ArrayList<>();
+            for (Object id : idList) {
+                if (id instanceof Integer) {
+                    ids.add(((Integer) id).longValue());
+                } else if (id instanceof Long) {
+                    ids.add((Long) id);
+                } else {
+                    ids.add(Long.parseLong(id.toString()));
+                }
+            }
+            
+            List<String> errors = wardAreaService.batchSetCode(ids, codes);
+            if (errors.isEmpty()) {
+                result.put("code", 200);
+                result.put("message", "设置成功");
+            } else {
+                result.put("code", 200);
+                result.put("message", "部分设置成功");
+                result.put("errors", errors);
+            }
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "设置失败:" + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 清除缓存
+     */
+    @PostMapping("/clear-cache")
+    public Map<String, Object> clearCache() {
+        Map<String, Object> result = new HashMap<>();
+        wardAreaService.clearCache();
+        result.put("code", 200);
+        result.put("message", "缓存已清除");
+        return result;
+    }
+
+    /**
+     * 删除病区
+     */
+    @DeleteMapping("/delete/{id}")
+    public Map<String, Object> delete(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            wardAreaService.deleteById(id);
+            result.put("code", 200);
+            result.put("message", "删除成功");
+        } catch (Exception e) {
+            result.put("code", 500);
+            result.put("message", "删除失败:" + e.getMessage());
+        }
+        return result;
+    }
+}

+ 26 - 0
src/main/java/org/example/entity/Bed.java

@@ -0,0 +1,26 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 床位实体类 - 对应tb_hospital_bed表
+ */
+@Data
+public class Bed implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long id;
+    private String code;          // 床位代码(系统内部代码)
+    private String outCode;       // 床位外部代码(HIS系统床位号)
+    private String name;          // 床位名称
+    private String bedType;       // 床位类型(关联数据字典)
+    private String belongDept;    // 所属科室
+    private String belongWard;    // 所属病区
+    private String belongRoom;    // 所属病房
+    private String doctor;        // 管床医生
+    private String nurse;         // 管床护士
+    private Integer used;         // 使用状态:0-未使用,1-已使用
+    private Integer sort;         // 排序号
+    private Integer enabled;      // 是否启用:1-是,0-否
+    private String remark;        // 备注
+}

+ 22 - 0
src/main/java/org/example/entity/Department.java

@@ -0,0 +1,22 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 科室实体类 - 对应tb_hospital_dept表
+ */
+@Data
+public class Department implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long id;
+    private String code;          // 科室代码
+    private String outCode;       // 科室外部代码(HIS系统)
+    private String name;          // 科室名称
+    private String address;       // 科室地址
+    private String telephone;     // 科室电话
+    private String director;      // 科室主任
+    private String introduction;  // 科室介绍(富文本)
+    private Integer enabled;      // 是否启用:1-是,0-否
+    private String remark;        // 备注
+}

+ 18 - 0
src/main/java/org/example/entity/Hospital.java

@@ -0,0 +1,18 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 医院概况实体类 - 对应yiyuangaikuang表
+ */
+@Data
+public class Hospital implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long id;              // 医院名称(对应数据库id字段)
+    private String name;          // 医院名称(用于前端显示)
+    private String logo;          // 医院logo
+    private String address;       // 医院地址(对应dizhi字段)
+    private String phone;         // 联系电话(对应dianhua字段)
+    private String introduction;  // 医院介绍(对应jieshao字段)
+}

+ 107 - 0
src/main/java/org/example/entity/PageResult.java

@@ -0,0 +1,107 @@
+package org.example.entity;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 分页结果封装类
+ */
+public class PageResult<T> {
+    
+    /**
+     * 当前页数据
+     */
+    private List<T> records;
+    
+    /**
+     * 总记录数
+     */
+    private long total;
+    
+    /**
+     * 当前页码
+     */
+    private int page;
+    
+    /**
+     * 每页条数
+     */
+    private int pageSize;
+    
+    /**
+     * 总页数
+     */
+    private int totalPages;
+    
+    public PageResult() {
+    }
+    
+    public PageResult(List<T> records, long total, int page, int pageSize) {
+        this.records = records;
+        this.total = total;
+        this.page = page;
+        this.pageSize = pageSize;
+        this.totalPages = (int) Math.ceil((double) total / pageSize);
+    }
+    
+    /**
+     * 对列表进行内存分页
+     */
+    public static <T> PageResult<T> of(List<T> allData, int page, int pageSize) {
+        if (allData == null || allData.isEmpty()) {
+            return new PageResult<>(Collections.emptyList(), 0, page, pageSize);
+        }
+        
+        int total = allData.size();
+        int fromIndex = (page - 1) * pageSize;
+        int toIndex = Math.min(fromIndex + pageSize, total);
+        
+        if (fromIndex >= total) {
+            return new PageResult<>(Collections.emptyList(), total, page, pageSize);
+        }
+        
+        List<T> records = new ArrayList<>(allData.subList(fromIndex, toIndex));
+        return new PageResult<>(records, total, page, pageSize);
+    }
+
+    public List<T> getRecords() {
+        return records;
+    }
+
+    public void setRecords(List<T> records) {
+        this.records = records;
+    }
+
+    public long getTotal() {
+        return total;
+    }
+
+    public void setTotal(long total) {
+        this.total = total;
+    }
+
+    public int getPage() {
+        return page;
+    }
+
+    public void setPage(int page) {
+        this.page = page;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(int pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public int getTotalPages() {
+        return totalPages;
+    }
+
+    public void setTotalPages(int totalPages) {
+        this.totalPages = totalPages;
+    }
+}

+ 38 - 0
src/main/java/org/example/entity/Patient.java

@@ -0,0 +1,38 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 患者实体类 - 对应tb_hospital_patient表
+ */
+@Data
+public class Patient implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long id;
+    
+    // 基本信息
+    private String name;                    // 患者姓名
+    private String sex;                     // 性别
+    private String age;                     // 年龄(支持多种格式:25天、2月5天、3岁6月、25岁)
+    private String medicalInsuranceType;   // 医保类型
+    private String deptCode;               // 所属科室
+    private String wardCode;               // 所属病区
+    private String roomNo;                 // 房间号
+    private String bedNo;                  // 床位号
+    private String doctorCode;             // 责任医生
+    private String nurseCode;              // 责任护士
+    private String inpatientNo;            // 住院号
+    private String inpatientSerialNo;      // 住院流水号
+    private Date admissionDateTime;        // 入院时间
+    private Date deptDateTime;             // 入科时间
+    private Date dischargedDateTime;       // 出院时间
+    private String inpatientStatus;        // 在院状态
+    private String diagnose;               // 入院诊断
+    private String conditionStatus;        // 病情状况
+    private String nurseLevel;             // 护理级别
+    private String allergen;               // 过敏信息
+    private String dietType;               // 饮食状况
+    private String notice;                 // 安全防范
+}

+ 26 - 0
src/main/java/org/example/entity/PatientCharge.java

@@ -0,0 +1,26 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 患者费用信息实体类 - 对应tb_patient_charges表
+ */
+@Data
+public class PatientCharge implements Serializable {
+    private static final long serialVersionUID = 1L;
+    
+    private Long id;
+    private Long patientId;              // 患者ID
+    private Date chargeDate;             // 费用产生日期
+    private String itemName;             // 项目名称
+    private BigDecimal amount;           // 金额
+    private Integer quantity;            // 数量
+    private String unit;                 // 单位
+    private BigDecimal subtotal;         // 小计
+    private String remark;               // 备注
+    private Integer isPaid;              // 是否缴费(0-未缴费,1-已缴费)
+    private Date payTime;                // 缴费时间
+}

+ 23 - 0
src/main/java/org/example/entity/PatientDiagnosis.java

@@ -0,0 +1,23 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 患者诊断信息实体类 - 对应tb_patient_diagnosis表
+ */
+@Data
+public class PatientDiagnosis implements Serializable {
+    private static final long serialVersionUID = 1L;
+    
+    private Long id;
+    private Long patientId;              // 患者ID
+    private String idCard;               // 身份证号
+    private String diagnoseType;         // 诊断类型
+    private Date diagnoseTime;           // 诊断时间
+    private String diagnoseDesc;         // 诊断描述
+    private Integer diagnoseStatus;      // 诊断状态(1-确诊,2-疑似,3-排除,4-待查)
+    private String diagnoseDoctor;       // 诊断医生
+    private String remark;               // 备注
+}

+ 28 - 0
src/main/java/org/example/entity/PatientExam.java

@@ -0,0 +1,28 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 患者检查报告实体类 - 对应tb_patient_exams表
+ */
+@Data
+public class PatientExam implements Serializable {
+    private static final long serialVersionUID = 1L;
+    
+    private Long id;
+    private Long patientId;              // 患者ID
+    private String examName;             // 检查名称
+    private String examPart;             // 检查部位
+    private String examMethod;           // 检查方式
+    private String microscopy;           // 光镜观察
+    private String imagingFindings;      // 影像所见
+    private String clinicalDiagnosis;    // 临床诊断
+    private String imagingDiagnosis;     // 影像诊断
+    private String examDoctor;           // 检查医生
+    private Date examTime;               // 检查时间
+    private String examiner;             // 审核者
+    private Date reportTime;             // 报告时间
+    private String remark;               // 备注
+}

+ 31 - 0
src/main/java/org/example/entity/PatientOrder.java

@@ -0,0 +1,31 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 患者医嘱信息实体类 - 对应tb_patient_orders表
+ */
+@Data
+public class PatientOrder implements Serializable {
+    private static final long serialVersionUID = 1L;
+    
+    private Long id;
+    private Long patientId;              // 患者ID
+    private Integer orderType;           // 医嘱类型(1-长期医嘱,2-临时医嘱,3-出院医嘱)
+    private String orderName;            // 医嘱名称
+    private String medicine;             // 药物
+    private String dosage;               // 用量
+    private String dosageUnit;           // 单位
+    private String medicationWay;        // 用药方式(口服、静脉注射、皮下注射)
+    private String medicationFreq;       // 用药频率
+    private Integer execStatus;          // 执行状态(1-未执行,2-执行中,3-已执行,4-已停止)
+    private Date startTime;              // 开始时间
+    private Date endTime;                // 结束时间
+    private String orderDoctor;          // 开嘱医生
+    private Date orderTime;              // 开嘱时间
+    private String stopDoctor;           // 停嘱医生
+    private Date stopTime;               // 停嘱时间
+    private String remark;               // 备注
+}

+ 27 - 0
src/main/java/org/example/entity/PatientTest.java

@@ -0,0 +1,27 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 患者检验报告实体类 - 对应tb_patient_tests表
+ */
+@Data
+public class PatientTest implements Serializable {
+    private static final long serialVersionUID = 1L;
+    
+    private Long id;
+    private String patientId;        // 患者ID(身份证号)
+    private String testNo;           // 检验编号
+    private String testType;         // 检验类型
+    private String testName;         // 检验名称
+    private String sampleType;       // 样品类型
+    private String sampleName;       // 样品名称
+    private String sampleCode;       // 样品编号
+    private String testDoctor;       // 检验医生
+    private Date testTime;           // 检验时间
+    private String tester;           // 审核者
+    private Date reportTime;         // 报告时间
+    private String remark;           // 备注
+}

+ 22 - 0
src/main/java/org/example/entity/PatientTestItem.java

@@ -0,0 +1,22 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 患者检验项目明细实体类 - 对应tb_patient_tests表中的项目明细字段
+ */
+@Data
+public class PatientTestItem implements Serializable {
+    private static final long serialVersionUID = 1L;
+    
+    private Long id;
+    private String testNo;           // 检验单号
+    private String itemName;         // 项目名称
+    private String itemAbbr;         // 英文缩写
+    private String resultValue;      // 结果值
+    private String resultHint;       // 结果提示(偏高、偏低、正常等)
+    private String resultUnit;       // 单位
+    private String refRange;         // 参考范围
+    private String itemRemark;       // 备注
+}

+ 22 - 0
src/main/java/org/example/entity/Room.java

@@ -0,0 +1,22 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 病房实体类 - 对应tb_hospital_room表
+ */
+@Data
+public class Room implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long id;
+    private String code;          // 病房代码(系统内部代码)
+    private String outCode;       // 病房外部代码(HIS系统病房号)
+    private String name;          // 病房名称
+    private Integer bedCount;     // 额定床位
+    private String belongDept;    // 所属科室
+    private String belongWard;    // 所属病区
+    private Integer sort;         // 排序号
+    private Integer enabled;      // 是否启用:1-是,0-否
+    private String remark;        // 备注
+}

+ 24 - 0
src/main/java/org/example/entity/Terminal.java

@@ -0,0 +1,24 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 设备实体类 - 对应tb_terminal表
+ */
+@Data
+public class Terminal implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long id;
+    private String terminalType;      // 设备类型
+    private String terminalNumber;    // 设备编码
+    private String terminalDesc;      // 设备名称
+    private String deptCode;          // 所属科室
+    private String wardCode;          // 所属病区
+    private String ipAddress;         // IP地址
+    private String subnetMask;        // 子网掩码
+    private String gatewayAddress;    // 默认网关
+    private String macAddress;        // Mac地址
+    private String tenementName;      // 设备位置
+    private Integer isOnline;         // 在线状态:1-在线,0-离线
+}

+ 14 - 0
src/main/java/org/example/entity/Users.java

@@ -0,0 +1,14 @@
+package org.example.entity;
+
+import lombok.Data;
+
+/**
+ * 管理员实体类 - 对应users表
+ */
+@Data
+public class Users {
+    private Long id;
+    private String username;
+    private String password;
+    private String name;
+}

+ 26 - 0
src/main/java/org/example/entity/WardArea.java

@@ -0,0 +1,26 @@
+package org.example.entity;
+
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 病区实体类 - 对应tb_hospital_ward表
+ */
+@Data
+public class WardArea implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Long id;
+    private String code;          // 病区代码(系统内部代码)
+    private String outCode;       // 病区外部代码(HIS系统)
+    private String name;          // 病区名称
+    private String address;       // 病区地址
+    private String glkeshi;       // 关联科室(多个用逗号分隔)
+    private String telephone;     // 病区电话
+    private String director;      // 病区主任医生
+    private String headNurse;     // 病区护士长
+    private Integer bedCount;     // 编制床位数
+    private Integer bedOpenCount; // 开放床位数
+    private Integer sort;         // 排序号
+    private Integer enabled;      // 是否启用:1-是,0-否
+    private String remark;        // 备注
+}

+ 15 - 0
src/main/java/org/example/entity/Yonghu.java

@@ -0,0 +1,15 @@
+package org.example.entity;
+
+import lombok.Data;
+
+/**
+ * 用户实体类 - 对应yonghu表
+ */
+@Data
+public class Yonghu {
+    private Long id;
+    private String username;
+    private String password;
+    private String name;
+    private String phone;
+}

+ 67 - 0
src/main/java/org/example/mapper/BedMapper.java

@@ -0,0 +1,67 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.Bed;
+
+import java.util.List;
+
+/**
+ * 床位Mapper接口
+ */
+@Mapper
+public interface BedMapper {
+    
+    /**
+     * 查询床位列表
+     */
+    List<Bed> findList(@Param("code") String code,
+                       @Param("belongDept") String belongDept,
+                       @Param("belongWard") String belongWard,
+                       @Param("belongRoom") String belongRoom);
+    
+    /**
+     * 根据ID查询床位
+     */
+    Bed findById(@Param("id") Long id);
+    
+    /**
+     * 根据床位代码和所属病区查询(用于校验同病区下代码重复)
+     */
+    Bed findByCodeAndWard(@Param("code") String code, @Param("belongWard") String belongWard);
+    
+    /**
+     * 新增床位
+     */
+    int insert(Bed bed);
+    
+    /**
+     * 更新床位
+     */
+    int update(Bed bed);
+    
+    /**
+     * 更新床位代码
+     */
+    int updateCode(@Param("id") Long id, @Param("code") String code);
+    
+    /**
+     * 删除床位
+     */
+    int deleteById(@Param("id") Long id);
+    
+    /**
+     * 获取所有不重复的所属科室
+     */
+    List<String> findAllBelongDept();
+    
+    /**
+     * 根据科室获取病区列表
+     */
+    List<String> findWardsByDept(@Param("belongDept") String belongDept);
+    
+    /**
+     * 根据病区获取病房列表
+     */
+    List<String> findRoomsByWard(@Param("belongWard") String belongWard);
+}

+ 39 - 0
src/main/java/org/example/mapper/DepartmentMapper.java

@@ -0,0 +1,39 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.Department;
+
+import java.util.List;
+
+/**
+ * 科室Mapper接口
+ */
+@Mapper
+public interface DepartmentMapper {
+    
+    /**
+     * 查询科室列表
+     */
+    List<Department> findList(@Param("name") String name, @Param("enabled") Integer enabled);
+    
+    /**
+     * 根据ID查询科室
+     */
+    Department findById(@Param("id") Long id);
+    
+    /**
+     * 新增科室
+     */
+    int insert(Department department);
+    
+    /**
+     * 更新科室
+     */
+    int update(Department department);
+    
+    /**
+     * 删除科室
+     */
+    int deleteById(@Param("id") Long id);
+}

+ 27 - 0
src/main/java/org/example/mapper/HospitalMapper.java

@@ -0,0 +1,27 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.Hospital;
+
+/**
+ * 医院概况Mapper接口
+ */
+@Mapper
+public interface HospitalMapper {
+    
+    /**
+     * 获取医院信息(只有一条记录)
+     */
+    Hospital getHospital();
+    
+    /**
+     * 新增医院信息
+     */
+    int insert(Hospital hospital);
+    
+    /**
+     * 更新医院信息
+     */
+    int update(@Param("hospital") Hospital hospital, @Param("oldName") String oldName);
+}

+ 39 - 0
src/main/java/org/example/mapper/PatientChargeMapper.java

@@ -0,0 +1,39 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.PatientCharge;
+
+import java.util.List;
+
+/**
+ * 患者费用信息Mapper接口
+ */
+@Mapper
+public interface PatientChargeMapper {
+    
+    /**
+     * 根据患者ID查询费用信息列表
+     */
+    List<PatientCharge> findByPatientId(@Param("patientId") Long patientId);
+    
+    /**
+     * 根据ID查询费用信息
+     */
+    PatientCharge findById(@Param("id") Long id);
+    
+    /**
+     * 新增费用信息
+     */
+    int insert(PatientCharge charge);
+    
+    /**
+     * 更新费用信息
+     */
+    int update(PatientCharge charge);
+    
+    /**
+     * 删除费用信息
+     */
+    int deleteById(@Param("id") Long id);
+}

+ 44 - 0
src/main/java/org/example/mapper/PatientDiagnosisMapper.java

@@ -0,0 +1,44 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.PatientDiagnosis;
+
+import java.util.List;
+
+/**
+ * 患者诊断信息Mapper接口
+ */
+@Mapper
+public interface PatientDiagnosisMapper {
+    
+    /**
+     * 根据患者ID查询诊断信息列表
+     */
+    List<PatientDiagnosis> findByPatientId(@Param("patientId") Long patientId);
+
+    /**
+     * 根据身份证号查询诊断信息列表
+     */
+    List<PatientDiagnosis> findByIdCard(@Param("idCard") String idCard);
+    
+    /**
+     * 根据ID查询诊断信息
+     */
+    PatientDiagnosis findById(@Param("id") Long id);
+    
+    /**
+     * 新增诊断信息
+     */
+    int insert(PatientDiagnosis diagnosis);
+    
+    /**
+     * 更新诊断信息
+     */
+    int update(PatientDiagnosis diagnosis);
+    
+    /**
+     * 删除诊断信息
+     */
+    int deleteById(@Param("id") Long id);
+}

+ 39 - 0
src/main/java/org/example/mapper/PatientExamMapper.java

@@ -0,0 +1,39 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.PatientExam;
+
+import java.util.List;
+
+/**
+ * 患者检查报告Mapper接口
+ */
+@Mapper
+public interface PatientExamMapper {
+    
+    /**
+     * 根据患者ID查询检查报告列表
+     */
+    List<PatientExam> findByPatientId(@Param("patientId") Long patientId);
+    
+    /**
+     * 根据ID查询检查报告
+     */
+    PatientExam findById(@Param("id") Long id);
+    
+    /**
+     * 新增检查报告
+     */
+    int insert(PatientExam exam);
+    
+    /**
+     * 更新检查报告
+     */
+    int update(PatientExam exam);
+    
+    /**
+     * 删除检查报告
+     */
+    int deleteById(@Param("id") Long id);
+}

+ 79 - 0
src/main/java/org/example/mapper/PatientMapper.java

@@ -0,0 +1,79 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.Patient;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 患者Mapper接口
+ */
+@Mapper
+public interface PatientMapper {
+    
+    /**
+     * 查询患者列表
+     */
+    List<Patient> findList(@Param("name") String name,
+                           @Param("deptCode") String deptCode,
+                           @Param("wardCode") String wardCode,
+                           @Param("roomNo") String roomNo,
+                           @Param("bedNo") String bedNo,
+                           @Param("inpatientNo") String inpatientNo,
+                           @Param("nurseLevel") String nurseLevel,
+                           @Param("inpatientStatus") String inpatientStatus,
+                           @Param("admissionDateStart") Date admissionDateStart,
+                           @Param("admissionDateEnd") Date admissionDateEnd);
+    
+    /**
+     * 根据ID查询患者
+     */
+    Patient findById(@Param("id") Long id);
+    
+    /**
+     * 新增患者
+     */
+    int insert(Patient patient);
+    
+    /**
+     * 更新患者
+     */
+    int update(Patient patient);
+    
+    /**
+     * 删除患者
+     */
+    int deleteById(@Param("id") Long id);
+    
+    /**
+     * 获取所有不重复的所属科室
+     */
+    List<String> findAllDeptCode();
+    
+    /**
+     * 根据科室获取病区列表
+     */
+    List<String> findWardsByDept(@Param("deptCode") String deptCode);
+    
+    /**
+     * 根据病区获取房间号列表
+     */
+    List<String> findRoomsByWard(@Param("wardCode") String wardCode);
+    
+    /**
+     * 根据房间号获取床位号列表
+     */
+    List<String> findBedsByRoom(@Param("roomNo") String roomNo);
+    
+    /**
+     * 获取所有不重复的在院状态
+     */
+    List<String> findAllInpatientStatus();
+    
+    /**
+     * 获取所有不重复的护理级别
+     */
+    List<String> findAllNurseLevel();
+}

+ 39 - 0
src/main/java/org/example/mapper/PatientOrderMapper.java

@@ -0,0 +1,39 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.PatientOrder;
+
+import java.util.List;
+
+/**
+ * 患者医嘱信息Mapper接口
+ */
+@Mapper
+public interface PatientOrderMapper {
+    
+    /**
+     * 根据患者ID查询医嘱信息列表
+     */
+    List<PatientOrder> findByPatientId(@Param("patientId") Long patientId);
+    
+    /**
+     * 根据ID查询医嘱信息
+     */
+    PatientOrder findById(@Param("id") Long id);
+    
+    /**
+     * 新增医嘱信息
+     */
+    int insert(PatientOrder order);
+    
+    /**
+     * 更新医嘱信息
+     */
+    int update(PatientOrder order);
+    
+    /**
+     * 删除医嘱信息
+     */
+    int deleteById(@Param("id") Long id);
+}

+ 24 - 0
src/main/java/org/example/mapper/PatientTestItemMapper.java

@@ -0,0 +1,24 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.PatientTestItem;
+
+import java.util.List;
+
+/**
+ * 患者检验项目明细Mapper接口
+ */
+@Mapper
+public interface PatientTestItemMapper {
+    
+    /**
+     * 根据检验报告ID查询项目明细列表(通过test_no关联)
+     */
+    List<PatientTestItem> findByTestId(@Param("testId") Long testId);
+    
+    /**
+     * 根据 ID查询项目明细
+     */
+    PatientTestItem findById(@Param("id") Long id);
+}

+ 44 - 0
src/main/java/org/example/mapper/PatientTestMapper.java

@@ -0,0 +1,44 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.PatientTest;
+
+import java.util.List;
+
+/**
+ * 患者检验报告Mapper接口
+ */
+@Mapper
+public interface PatientTestMapper {
+    
+    /**
+     * 根据患者ID(身份证号)查询检验报告列表
+     */
+    List<PatientTest> findByPatientId(@Param("patientId") String patientId);
+
+    /**
+     * 根据患者表ID查询检验报告列表(通过诊断表关联身份证号)
+     */
+    List<PatientTest> findByPatientTableId(@Param("patientTableId") Long patientTableId);
+    
+    /**
+     * 根据ID查询检验报告
+     */
+    PatientTest findById(@Param("id") Long id);
+    
+    /**
+     * 新增检验报告
+     */
+    int insert(PatientTest test);
+    
+    /**
+     * 更新检验报告
+     */
+    int update(PatientTest test);
+    
+    /**
+     * 删除检验报告
+     */
+    int deleteById(@Param("id") Long id);
+}

+ 62 - 0
src/main/java/org/example/mapper/RoomMapper.java

@@ -0,0 +1,62 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.Room;
+
+import java.util.List;
+
+/**
+ * 病房Mapper接口
+ */
+@Mapper
+public interface RoomMapper {
+    
+    /**
+     * 查询病房列表
+     */
+    List<Room> findList(@Param("code") String code,
+                        @Param("belongDept") String belongDept,
+                        @Param("belongWard") String belongWard,
+                        @Param("enabled") Integer enabled);
+    
+    /**
+     * 根据ID查询病房
+     */
+    Room findById(@Param("id") Long id);
+    
+    /**
+     * 根据病房代码和所属病区查询(用于校验同病区下代码重复)
+     */
+    Room findByCodeAndWard(@Param("code") String code, @Param("belongWard") String belongWard);
+    
+    /**
+     * 新增病房
+     */
+    int insert(Room room);
+    
+    /**
+     * 更新病房
+     */
+    int update(Room room);
+    
+    /**
+     * 更新病房代码
+     */
+    int updateCode(@Param("id") Long id, @Param("code") String code);
+    
+    /**
+     * 删除病房
+     */
+    int deleteById(@Param("id") Long id);
+    
+    /**
+     * 获取所有不重复的所属科室
+     */
+    List<String> findAllBelongDept();
+    
+    /**
+     * 根据科室获取病区列表
+     */
+    List<String> findWardsByDept(@Param("belongDept") String belongDept);
+}

+ 58 - 0
src/main/java/org/example/mapper/TerminalMapper.java

@@ -0,0 +1,58 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.Terminal;
+
+import java.util.List;
+
+/**
+ * 设备Mapper接口
+ */
+@Mapper
+public interface TerminalMapper {
+    
+    /**
+     * 查询设备列表
+     */
+    List<Terminal> findList(@Param("terminalType") String terminalType,
+                            @Param("deptCode") String deptCode,
+                            @Param("wardCode") String wardCode,
+                            @Param("isOnline") Integer isOnline,
+                            @Param("terminalDesc") String terminalDesc);
+    
+    /**
+     * 根据ID查询设备
+     */
+    Terminal findById(@Param("id") Long id);
+    
+    /**
+     * 获取所有不重复的设备类型
+     */
+    List<String> findAllTerminalTypes();
+    
+    /**
+     * 获取所有不重复的所属科室
+     */
+    List<String> findAllDeptCode();
+    
+    /**
+     * 根据科室获取病区列表
+     */
+    List<String> findWardsByDept(@Param("deptCode") String deptCode);
+    
+    /**
+     * 新增设备
+     */
+    int insert(Terminal terminal);
+    
+    /**
+     * 检查IP地址是否已存在
+     */
+    int checkIpExists(@Param("ipAddress") String ipAddress);
+    
+    /**
+     * 获取指定前缀的设备编码数量(用于生成编码)
+     */
+    int countByTerminalNumberPrefix(@Param("prefix") String prefix);
+}

+ 17 - 0
src/main/java/org/example/mapper/UsersMapper.java

@@ -0,0 +1,17 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.Users;
+
+/**
+ * 管理员Mapper接口
+ */
+@Mapper
+public interface UsersMapper {
+    
+    /**
+     * 根据用户名和密码查询管理员
+     */
+    Users findByUsernameAndPassword(@Param("username") String username, @Param("password") String password);
+}

+ 56 - 0
src/main/java/org/example/mapper/WardAreaMapper.java

@@ -0,0 +1,56 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.WardArea;
+
+import java.util.List;
+
+/**
+ * 病区Mapper接口
+ */
+@Mapper
+public interface WardAreaMapper {
+    
+    /**
+     * 查询病区列表
+     */
+    List<WardArea> findList(@Param("name") String name, 
+                            @Param("glkeshi") String glkeshi,
+                            @Param("enabled") Integer enabled);
+    
+    /**
+     * 根据ID查询病区
+     */
+    WardArea findById(@Param("id") Long id);
+    
+    /**
+     * 根据病区代码查询(用于校验重复)
+     */
+    WardArea findByCode(@Param("code") String code);
+    
+    /**
+     * 新增病区
+     */
+    int insert(WardArea wardArea);
+    
+    /**
+     * 更新病区
+     */
+    int update(WardArea wardArea);
+    
+    /**
+     * 更新病区代码
+     */
+    int updateCode(@Param("id") Long id, @Param("code") String code);
+    
+    /**
+     * 删除病区
+     */
+    int deleteById(@Param("id") Long id);
+    
+    /**
+     * 获取所有不重复的关联科室
+     */
+    List<String> findAllGlkeshi();
+}

+ 17 - 0
src/main/java/org/example/mapper/YonghuMapper.java

@@ -0,0 +1,17 @@
+package org.example.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.example.entity.Yonghu;
+
+/**
+ * 用户Mapper接口
+ */
+@Mapper
+public interface YonghuMapper {
+    
+    /**
+     * 根据用户名和密码查询用户
+     */
+    Yonghu findByUsernameAndPassword(@Param("username") String username, @Param("password") String password);
+}

+ 146 - 0
src/main/java/org/example/service/BedService.java

@@ -0,0 +1,146 @@
+package org.example.service;
+
+import org.example.config.CacheConstants;
+import org.example.entity.Bed;
+import org.example.mapper.BedMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 床位服务
+ */
+@Service
+public class BedService {
+
+    @Autowired
+    private BedMapper bedMapper;
+
+    /**
+     * 查询床位列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_BED, key = "'list:' + #code + ':' + #belongDept + ':' + #belongWard + ':' + #belongRoom")
+    public List<Bed> findList(String code, String belongDept, String belongWard, String belongRoom) {
+        return bedMapper.findList(code, belongDept, belongWard, belongRoom);
+    }
+
+    /**
+     * 根据ID查询床位
+     */
+    @Cacheable(value = CacheConstants.CACHE_BED, key = "'id:' + #id")
+    public Bed findById(Long id) {
+        return bedMapper.findById(id);
+    }
+
+    /**
+     * 保存床位
+     */
+    @CacheEvict(value = {CacheConstants.CACHE_BED, CacheConstants.CACHE_OPTIONS}, allEntries = true)
+    public void save(Bed bed) {
+        if (bed.getOutCode() == null || bed.getOutCode().isEmpty()) {
+            bed.setOutCode(bed.getCode());
+        }
+        if (bed.getId() == null) {
+            bedMapper.insert(bed);
+        } else {
+            bedMapper.update(bed);
+        }
+    }
+
+    /**
+     * 批量设置床位代码
+     * @return 返回设置失败的信息
+     */
+    @CacheEvict(value = CacheConstants.CACHE_BED, allEntries = true)
+    public List<String> batchSetCode(List<Long> ids, List<String> codes) {
+        List<String> errors = new ArrayList<>();
+        
+        for (int i = 0; i < ids.size(); i++) {
+            Long id = ids.get(i);
+            String code = codes.get(i);
+            
+            // 格式化代码为三位数字
+            code = formatCode(code);
+            
+            // 校验代码格式
+            if (!code.matches("^\\d{3}$")) {
+                Bed bed = bedMapper.findById(id);
+                errors.add("床位[" + (bed != null ? bed.getName() : id) + "]代码格式错误,应为001~999");
+                continue;
+            }
+            
+            // 获取床位信息以获取所属病区
+            Bed bed = bedMapper.findById(id);
+            if (bed == null) {
+                errors.add("床位ID[" + id + "]不存在");
+                continue;
+            }
+            
+            // 校验同病区下代码是否重复
+            Bed existing = bedMapper.findByCodeAndWard(code, bed.getBelongWard());
+            if (existing != null && !existing.getId().equals(id)) {
+                errors.add("床位[" + bed.getName() + "]代码[" + code + "]在病区[" + bed.getBelongWard() + "]下已被使用");
+                continue;
+            }
+            
+            // 更新代码
+            bedMapper.updateCode(id, code);
+        }
+        
+        return errors;
+    }
+
+    /**
+     * 格式化床位代码为三位数字
+     */
+    private String formatCode(String code) {
+        if (code == null || code.isEmpty()) {
+            return "000";
+        }
+        // 去除非数字字符
+        code = code.replaceAll("\\D", "");
+        if (code.isEmpty()) {
+            return "000";
+        }
+        int num = Integer.parseInt(code);
+        if (num > 999) num = 999;
+        if (num < 0) num = 0;
+        return String.format("%03d", num);
+    }
+
+    /**
+     * 删除床位
+     */
+    @CacheEvict(value = {CacheConstants.CACHE_BED, CacheConstants.CACHE_OPTIONS}, allEntries = true)
+    public void deleteById(Long id) {
+        bedMapper.deleteById(id);
+    }
+
+    /**
+     * 获取所有不重复的所属科室
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'bed:dept'")
+    public List<String> findAllBelongDept() {
+        return bedMapper.findAllBelongDept();
+    }
+
+    /**
+     * 根据科室获取病区列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'bed:ward:' + #belongDept")
+    public List<String> findWardsByDept(String belongDept) {
+        return bedMapper.findWardsByDept(belongDept);
+    }
+
+    /**
+     * 根据病区获取病房列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'bed:room:' + #belongWard")
+    public List<String> findRoomsByWard(String belongWard) {
+        return bedMapper.findRoomsByWard(belongWard);
+    }
+}

+ 61 - 0
src/main/java/org/example/service/DepartmentService.java

@@ -0,0 +1,61 @@
+package org.example.service;
+
+import org.example.config.CacheConstants;
+import org.example.entity.Department;
+import org.example.mapper.DepartmentMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 科室服务
+ */
+@Service
+public class DepartmentService {
+
+    @Autowired
+    private DepartmentMapper departmentMapper;
+
+    /**
+     * 查询科室列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_DEPARTMENT, key = "'list:' + #name + ':' + #enabled")
+    public List<Department> findList(String name, Integer enabled) {
+        return departmentMapper.findList(name, enabled);
+    }
+
+    /**
+     * 根据ID查询科室
+     */
+    @Cacheable(value = CacheConstants.CACHE_DEPARTMENT, key = "'id:' + #id")
+    public Department findById(Long id) {
+        return departmentMapper.findById(id);
+    }
+
+    /**
+     * 保存科室
+     */
+    @CacheEvict(value = CacheConstants.CACHE_DEPARTMENT, allEntries = true)
+    public void save(Department department) {
+        // 如果外部代码为空,默认等于科室代码
+        if (department.getOutCode() == null || department.getOutCode().isEmpty()) {
+            department.setOutCode(department.getCode());
+        }
+        if (department.getId() == null) {
+            departmentMapper.insert(department);
+        } else {
+            departmentMapper.update(department);
+        }
+    }
+
+    /**
+     * 删除科室
+     */
+    @CacheEvict(value = CacheConstants.CACHE_DEPARTMENT, allEntries = true)
+    public void deleteById(Long id) {
+        departmentMapper.deleteById(id);
+    }
+}

+ 41 - 0
src/main/java/org/example/service/HospitalService.java

@@ -0,0 +1,41 @@
+package org.example.service;
+
+import org.example.config.CacheConstants;
+import org.example.entity.Hospital;
+import org.example.mapper.HospitalMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+/**
+ * 医院概况服务
+ */
+@Service
+public class HospitalService {
+
+    @Autowired
+    private HospitalMapper hospitalMapper;
+
+    /**
+     * 获取医院信息
+     */
+    @Cacheable(value = CacheConstants.CACHE_HOSPITAL, key = "'info'")
+    public Hospital getHospital() {
+        return hospitalMapper.getHospital();
+    }
+
+    /**
+     * 保存医院信息(新增或更新)
+     */
+    @CacheEvict(value = CacheConstants.CACHE_HOSPITAL, allEntries = true)
+    public void saveHospital(Hospital hospital) {
+        Hospital existing = hospitalMapper.getHospital();
+        if (existing == null) {
+            hospitalMapper.insert(hospital);
+        } else {
+            // 使用旧名称作为WHERE条件进行更新
+            hospitalMapper.update(hospital, existing.getName());
+        }
+    }
+}

+ 60 - 0
src/main/java/org/example/service/LoginService.java

@@ -0,0 +1,60 @@
+package org.example.service;
+
+import org.example.entity.Users;
+import org.example.entity.Yonghu;
+import org.example.mapper.UsersMapper;
+import org.example.mapper.YonghuMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 登录服务
+ */
+@Service
+public class LoginService {
+
+    @Autowired
+    private YonghuMapper yonghuMapper;
+
+    @Autowired
+    private UsersMapper usersMapper;
+
+    /**
+     * 用户登录
+     */
+    public Map<String, Object> userLogin(String username, String password) {
+        Map<String, Object> result = new HashMap<>();
+        Yonghu yonghu = yonghuMapper.findByUsernameAndPassword(username, password);
+        if (yonghu != null) {
+            result.put("code", 200);
+            result.put("message", "登录成功");
+            result.put("data", yonghu);
+            result.put("userType", "user");
+        } else {
+            result.put("code", 500);
+            result.put("message", "用户名或密码错误");
+        }
+        return result;
+    }
+
+    /**
+     * 管理员登录
+     */
+    public Map<String, Object> adminLogin(String username, String password) {
+        Map<String, Object> result = new HashMap<>();
+        Users admin = usersMapper.findByUsernameAndPassword(username, password);
+        if (admin != null) {
+            result.put("code", 200);
+            result.put("message", "登录成功");
+            result.put("data", admin);
+            result.put("userType", "admin");
+        } else {
+            result.put("code", 500);
+            result.put("message", "管理员账号或密码错误");
+        }
+        return result;
+    }
+}

+ 50 - 0
src/main/java/org/example/service/PatientChargeService.java

@@ -0,0 +1,50 @@
+package org.example.service;
+
+import org.example.entity.PatientCharge;
+import org.example.mapper.PatientChargeMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 患者费用信息服务
+ */
+@Service
+public class PatientChargeService {
+
+    @Autowired
+    private PatientChargeMapper patientChargeMapper;
+
+    /**
+     * 根据患者ID查询费用信息列表
+     */
+    public List<PatientCharge> findByPatientId(Long patientId) {
+        return patientChargeMapper.findByPatientId(patientId);
+    }
+
+    /**
+     * 根据ID查询费用信息
+     */
+    public PatientCharge findById(Long id) {
+        return patientChargeMapper.findById(id);
+    }
+
+    /**
+     * 保存费用信息
+     */
+    public void save(PatientCharge charge) {
+        if (charge.getId() == null) {
+            patientChargeMapper.insert(charge);
+        } else {
+            patientChargeMapper.update(charge);
+        }
+    }
+
+    /**
+     * 删除费用信息
+     */
+    public void deleteById(Long id) {
+        patientChargeMapper.deleteById(id);
+    }
+}

+ 57 - 0
src/main/java/org/example/service/PatientDiagnosisService.java

@@ -0,0 +1,57 @@
+package org.example.service;
+
+import org.example.entity.PatientDiagnosis;
+import org.example.mapper.PatientDiagnosisMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 患者诊断信息服务
+ */
+@Service
+public class PatientDiagnosisService {
+
+    @Autowired
+    private PatientDiagnosisMapper patientDiagnosisMapper;
+
+    /**
+     * 根据患者ID查询诊断信息列表
+     */
+    public List<PatientDiagnosis> findByPatientId(Long patientId) {
+        return patientDiagnosisMapper.findByPatientId(patientId);
+    }
+
+    /**
+     * 根据身份证号查询诊断信息列表
+     */
+    public List<PatientDiagnosis> findByIdCard(String idCard) {
+        return patientDiagnosisMapper.findByIdCard(idCard);
+    }
+
+    /**
+     * 根据ID查询诊断信息
+     */
+    public PatientDiagnosis findById(Long id) {
+        return patientDiagnosisMapper.findById(id);
+    }
+
+    /**
+     * 保存诊断信息
+     */
+    public void save(PatientDiagnosis diagnosis) {
+        if (diagnosis.getId() == null) {
+            patientDiagnosisMapper.insert(diagnosis);
+        } else {
+            patientDiagnosisMapper.update(diagnosis);
+        }
+    }
+
+    /**
+     * 删除诊断信息
+     */
+    public void deleteById(Long id) {
+        patientDiagnosisMapper.deleteById(id);
+    }
+}

+ 50 - 0
src/main/java/org/example/service/PatientExamService.java

@@ -0,0 +1,50 @@
+package org.example.service;
+
+import org.example.entity.PatientExam;
+import org.example.mapper.PatientExamMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 患者检查报告服务
+ */
+@Service
+public class PatientExamService {
+
+    @Autowired
+    private PatientExamMapper patientExamMapper;
+
+    /**
+     * 根据患者ID查询检查报告列表
+     */
+    public List<PatientExam> findByPatientId(Long patientId) {
+        return patientExamMapper.findByPatientId(patientId);
+    }
+
+    /**
+     * 根据ID查询检查报告
+     */
+    public PatientExam findById(Long id) {
+        return patientExamMapper.findById(id);
+    }
+
+    /**
+     * 保存检查报告
+     */
+    public void save(PatientExam exam) {
+        if (exam.getId() == null) {
+            patientExamMapper.insert(exam);
+        } else {
+            patientExamMapper.update(exam);
+        }
+    }
+
+    /**
+     * 删除检查报告
+     */
+    public void deleteById(Long id) {
+        patientExamMapper.deleteById(id);
+    }
+}

+ 50 - 0
src/main/java/org/example/service/PatientOrderService.java

@@ -0,0 +1,50 @@
+package org.example.service;
+
+import org.example.entity.PatientOrder;
+import org.example.mapper.PatientOrderMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 患者医嘱信息服务
+ */
+@Service
+public class PatientOrderService {
+
+    @Autowired
+    private PatientOrderMapper patientOrderMapper;
+
+    /**
+     * 根据患者ID查询医嘱信息列表
+     */
+    public List<PatientOrder> findByPatientId(Long patientId) {
+        return patientOrderMapper.findByPatientId(patientId);
+    }
+
+    /**
+     * 根据ID查询医嘱信息
+     */
+    public PatientOrder findById(Long id) {
+        return patientOrderMapper.findById(id);
+    }
+
+    /**
+     * 保存医嘱信息
+     */
+    public void save(PatientOrder order) {
+        if (order.getId() == null) {
+            patientOrderMapper.insert(order);
+        } else {
+            patientOrderMapper.update(order);
+        }
+    }
+
+    /**
+     * 删除医嘱信息
+     */
+    public void deleteById(Long id) {
+        patientOrderMapper.deleteById(id);
+    }
+}

+ 128 - 0
src/main/java/org/example/service/PatientService.java

@@ -0,0 +1,128 @@
+package org.example.service;
+
+import org.example.config.CacheConstants;
+import org.example.entity.PageResult;
+import org.example.entity.Patient;
+import org.example.mapper.PatientMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 患者服务
+ */
+@Service
+public class PatientService {
+
+    @Autowired
+    private PatientMapper patientMapper;
+
+    /**
+     * 获取页面初始化数据(合并查询,带缓存,支持分页)
+     */
+    public Map<String, Object> getInitData(int page, int pageSize) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("deptList", patientMapper.findAllDeptCode());
+        data.put("wardList", patientMapper.findWardsByDept(null));
+        data.put("roomList", patientMapper.findRoomsByWard(null));
+        data.put("bedList", patientMapper.findBedsByRoom(null));
+        data.put("inpatientStatusList", patientMapper.findAllInpatientStatus());
+        data.put("nurseLevelList", patientMapper.findAllNurseLevel());
+        // 分页处理患者列表
+        List<Patient> allPatients = patientMapper.findList(null, null, null, null, null, null, null, null, null, null);
+        data.put("patientList", PageResult.of(allPatients, page, pageSize));
+        return data;
+    }
+
+    /**
+     * 查询患者列表
+     */
+    public List<Patient> findList(String name, String deptCode, String wardCode, String roomNo,
+                                   String bedNo, String inpatientNo, String nurseLevel,
+                                   String inpatientStatus, Date admissionDateStart, Date admissionDateEnd) {
+        return patientMapper.findList(name, deptCode, wardCode, roomNo, bedNo, inpatientNo,
+                nurseLevel, inpatientStatus, admissionDateStart, admissionDateEnd);
+    }
+
+    /**
+     * 根据ID查询患者
+     */
+    @Cacheable(value = CacheConstants.CACHE_PATIENT, key = "'id:' + #id")
+    public Patient findById(Long id) {
+        return patientMapper.findById(id);
+    }
+
+    /**
+     * 保存患者
+     */
+    @CacheEvict(value = {CacheConstants.CACHE_PATIENT, CacheConstants.CACHE_OPTIONS}, allEntries = true)
+    public void save(Patient patient) {
+        if (patient.getId() == null) {
+            patientMapper.insert(patient);
+        } else {
+            patientMapper.update(patient);
+        }
+    }
+
+    /**
+     * 删除患者
+     */
+    @CacheEvict(value = {CacheConstants.CACHE_PATIENT, CacheConstants.CACHE_OPTIONS}, allEntries = true)
+    public void deleteById(Long id) {
+        patientMapper.deleteById(id);
+    }
+
+    /**
+     * 获取所有不重复的所属科室
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'patient:dept'")
+    public List<String> findAllDeptCode() {
+        return patientMapper.findAllDeptCode();
+    }
+
+    /**
+     * 根据科室获取病区列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'patient:ward:' + #deptCode")
+    public List<String> findWardsByDept(String deptCode) {
+        return patientMapper.findWardsByDept(deptCode);
+    }
+
+    /**
+     * 根据病区获取房间号列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'patient:room:' + #wardCode")
+    public List<String> findRoomsByWard(String wardCode) {
+        return patientMapper.findRoomsByWard(wardCode);
+    }
+
+    /**
+     * 根据房间号获取床位号列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'patient:bed:' + #roomNo")
+    public List<String> findBedsByRoom(String roomNo) {
+        return patientMapper.findBedsByRoom(roomNo);
+    }
+    
+    /**
+     * 获取所有不重复的在院状态
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'patient:inpatientStatus'")
+    public List<String> findAllInpatientStatus() {
+        return patientMapper.findAllInpatientStatus();
+    }
+    
+    /**
+     * 获取所有不重复的护理级别
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'patient:nurseLevel'")
+    public List<String> findAllNurseLevel() {
+        return patientMapper.findAllNurseLevel();
+    }
+}

+ 32 - 0
src/main/java/org/example/service/PatientTestItemService.java

@@ -0,0 +1,32 @@
+package org.example.service;
+
+import org.example.entity.PatientTestItem;
+import org.example.mapper.PatientTestItemMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 患者检验项目明细服务
+ */
+@Service
+public class PatientTestItemService {
+
+    @Autowired
+    private PatientTestItemMapper patientTestItemMapper;
+
+    /**
+     * 根据检验报告ID查询项目明细列表
+     */
+    public List<PatientTestItem> findByTestId(Long testId) {
+        return patientTestItemMapper.findByTestId(testId);
+    }
+
+    /**
+     * 根据ID查询项目明细
+     */
+    public PatientTestItem findById(Long id) {
+        return patientTestItemMapper.findById(id);
+    }
+}

+ 57 - 0
src/main/java/org/example/service/PatientTestService.java

@@ -0,0 +1,57 @@
+package org.example.service;
+
+import org.example.entity.PatientTest;
+import org.example.mapper.PatientTestMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 患者检验报告服务
+ */
+@Service
+public class PatientTestService {
+
+    @Autowired
+    private PatientTestMapper patientTestMapper;
+
+    /**
+     * 根据患者ID(身份证号)查询检验报告列表
+     */
+    public List<PatientTest> findByPatientId(String patientId) {
+        return patientTestMapper.findByPatientId(patientId);
+    }
+
+    /**
+     * 根据患者表ID查询检验报告列表
+     */
+    public List<PatientTest> findByPatientTableId(Long patientTableId) {
+        return patientTestMapper.findByPatientTableId(patientTableId);
+    }
+
+    /**
+     * 根据ID查询检验报告
+     */
+    public PatientTest findById(Long id) {
+        return patientTestMapper.findById(id);
+    }
+
+    /**
+     * 保存检验报告
+     */
+    public void save(PatientTest test) {
+        if (test.getId() == null) {
+            patientTestMapper.insert(test);
+        } else {
+            patientTestMapper.update(test);
+        }
+    }
+
+    /**
+     * 删除检验报告
+     */
+    public void deleteById(Long id) {
+        patientTestMapper.deleteById(id);
+    }
+}

+ 138 - 0
src/main/java/org/example/service/RoomService.java

@@ -0,0 +1,138 @@
+package org.example.service;
+
+import org.example.config.CacheConstants;
+import org.example.entity.Room;
+import org.example.mapper.RoomMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 病房服务
+ */
+@Service
+public class RoomService {
+
+    @Autowired
+    private RoomMapper roomMapper;
+
+    /**
+     * 查询病房列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_ROOM, key = "'list:' + #code + ':' + #belongDept + ':' + #belongWard + ':' + #enabled")
+    public List<Room> findList(String code, String belongDept, String belongWard, Integer enabled) {
+        return roomMapper.findList(code, belongDept, belongWard, enabled);
+    }
+
+    /**
+     * 根据ID查询病房
+     */
+    @Cacheable(value = CacheConstants.CACHE_ROOM, key = "'id:' + #id")
+    public Room findById(Long id) {
+        return roomMapper.findById(id);
+    }
+
+    /**
+     * 保存病房
+     */
+    @CacheEvict(value = {CacheConstants.CACHE_ROOM, CacheConstants.CACHE_OPTIONS}, allEntries = true)
+    public void save(Room room) {
+        if (room.getOutCode() == null || room.getOutCode().isEmpty()) {
+            room.setOutCode(room.getCode());
+        }
+        if (room.getId() == null) {
+            roomMapper.insert(room);
+        } else {
+            roomMapper.update(room);
+        }
+    }
+
+    /**
+     * 批量设置病房代码
+     * @return 返回设置失败的信息
+     */
+    @CacheEvict(value = CacheConstants.CACHE_ROOM, allEntries = true)
+    public List<String> batchSetCode(List<Long> ids, List<String> codes) {
+        List<String> errors = new ArrayList<>();
+        
+        for (int i = 0; i < ids.size(); i++) {
+            Long id = ids.get(i);
+            String code = codes.get(i);
+            
+            // 格式化代码为四位数字
+            code = formatCode(code);
+            
+            // 校验代码格式
+            if (!code.matches("^\\d{4}$")) {
+                Room room = roomMapper.findById(id);
+                errors.add("病房[" + (room != null ? room.getName() : id) + "]代码格式错误,应为0001~9999");
+                continue;
+            }
+            
+            // 获取病房信息以获取所属病区
+            Room room = roomMapper.findById(id);
+            if (room == null) {
+                errors.add("病房ID[" + id + "]不存在");
+                continue;
+            }
+            
+            // 校验同病区下代码是否重复
+            Room existing = roomMapper.findByCodeAndWard(code, room.getBelongWard());
+            if (existing != null && !existing.getId().equals(id)) {
+                errors.add("病房[" + room.getName() + "]代码[" + code + "]在病区[" + room.getBelongWard() + "]下已被使用");
+                continue;
+            }
+            
+            // 更新代码
+            roomMapper.updateCode(id, code);
+        }
+        
+        return errors;
+    }
+
+    /**
+     * 格式化病房代码为四位数字
+     */
+    private String formatCode(String code) {
+        if (code == null || code.isEmpty()) {
+            return "0000";
+        }
+        // 去除非数字字符
+        code = code.replaceAll("\\D", "");
+        if (code.isEmpty()) {
+            return "0000";
+        }
+        int num = Integer.parseInt(code);
+        if (num > 9999) num = 9999;
+        if (num < 0) num = 0;
+        return String.format("%04d", num);
+    }
+
+    /**
+     * 删除病房
+     */
+    @CacheEvict(value = {CacheConstants.CACHE_ROOM, CacheConstants.CACHE_OPTIONS}, allEntries = true)
+    public void deleteById(Long id) {
+        roomMapper.deleteById(id);
+    }
+
+    /**
+     * 获取所有不重复的所属科室
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'room:dept'")
+    public List<String> findAllBelongDept() {
+        return roomMapper.findAllBelongDept();
+    }
+
+    /**
+     * 根据科室获取病区列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'room:ward:' + #belongDept")
+    public List<String> findWardsByDept(String belongDept) {
+        return roomMapper.findWardsByDept(belongDept);
+    }
+}

+ 138 - 0
src/main/java/org/example/service/TerminalService.java

@@ -0,0 +1,138 @@
+package org.example.service;
+
+import org.example.config.CacheConstants;
+import org.example.entity.PageResult;
+import org.example.entity.Terminal;
+import org.example.mapper.TerminalMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.stereotype.Service;
+
+import org.springframework.cache.annotation.Cacheable;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备服务
+ */
+@Service
+public class TerminalService {
+
+    @Autowired
+    private TerminalMapper terminalMapper;
+
+    /**
+     * 获取页面初始化数据(合并查询,支持分页)
+     */
+    public Map<String, Object> getInitData(int page, int pageSize) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("deptList", terminalMapper.findAllDeptCode());
+        data.put("wardList", terminalMapper.findWardsByDept(null));
+        // 分页处理设备列表
+        List<Terminal> allTerminals = terminalMapper.findList(null, null, null, null, null);
+        data.put("terminalList", PageResult.of(allTerminals, page, pageSize));
+        return data;
+    }
+
+    /**
+     * 查询设备列表
+     */
+    public List<Terminal> findList(String terminalType, String deptCode, String wardCode,
+                                    Integer isOnline, String terminalDesc) {
+        return terminalMapper.findList(terminalType, deptCode, wardCode, isOnline, terminalDesc);
+    }
+
+    /**
+     * 根据ID查询设备
+     */
+    @Cacheable(value = CacheConstants.CACHE_TERMINAL, key = "'id:' + #id")
+    public Terminal findById(Long id) {
+        return terminalMapper.findById(id);
+    }
+
+    /**
+     * 获取所有不重复的设备类型
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'terminal:types'")
+    public List<String> findAllTerminalTypes() {
+        return terminalMapper.findAllTerminalTypes();
+    }
+
+    /**
+     * 获取所有不重复的所属科室
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'terminal:dept'")
+    public List<String> findAllDeptCode() {
+        return terminalMapper.findAllDeptCode();
+    }
+
+    /**
+     * 根据科室获取病区列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'terminal:ward:' + #deptCode")
+    public List<String> findWardsByDept(String deptCode) {
+        return terminalMapper.findWardsByDept(deptCode);
+    }
+    
+    /**
+     * 新增设备
+     * @return null表示成功,否则返回错误信息
+     */
+    @CacheEvict(value = CacheConstants.CACHE_TERMINAL, allEntries = true)
+    public String addTerminal(Terminal terminal) {
+        // 校验IP地址是否重复
+        if (terminal.getIpAddress() != null && !terminal.getIpAddress().isEmpty()) {
+            int count = terminalMapper.checkIpExists(terminal.getIpAddress());
+            if (count > 0) {
+                return "IP地址存在重复";
+            }
+        }
+        
+        // 生成设备编码
+        String terminalNumber = generateTerminalNumber(terminal);
+        terminal.setTerminalNumber(terminalNumber);
+        
+        // 设置默认在线状态为离线
+        if (terminal.getIsOnline() == null) {
+            terminal.setIsOnline(0);
+        }
+        
+        terminalMapper.insert(terminal);
+        return null;
+    }
+    
+    /**
+     * 生成设备编码
+     * 规则:设备类型编号 + 病区代码 + 病房代码 + 床位号 / 设备号
+     */
+    private String generateTerminalNumber(Terminal terminal) {
+        StringBuilder sb = new StringBuilder();
+        String type = terminal.getTerminalType();
+        
+        // 设备类型编号
+        sb.append(type != null ? type : "");
+        
+        // 病区代码(从terminalDesc中解析或使用wardCode)
+        String wardCode = terminal.getWardCode();
+        if (wardCode != null && !wardCode.isEmpty()) {
+            sb.append(wardCode);
+        }
+        
+        // 根据设备名称解析其他部分
+        String desc = terminal.getTerminalDesc();
+        if (desc != null) {
+            sb.append(desc);
+        }
+        
+        return sb.toString();
+    }
+    
+    /**
+     * 检查IP地址是否存在
+     */
+    public boolean checkIpExists(String ipAddress) {
+        return terminalMapper.checkIpExists(ipAddress) > 0;
+    }
+}

+ 149 - 0
src/main/java/org/example/service/WardAreaService.java

@@ -0,0 +1,149 @@
+package org.example.service;
+
+import org.example.config.CacheConstants;
+import org.example.entity.WardArea;
+import org.example.mapper.WardAreaMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 病区服务
+ */
+@Service
+public class WardAreaService {
+
+    @Autowired
+    private WardAreaMapper wardAreaMapper;
+
+    /**
+     * 查询病区列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_WARD_AREA, key = "'list:' + #name + ':' + #glkeshi + ':' + #enabled")
+    public List<WardArea> findList(String name, String glkeshi, Integer enabled) {
+        return wardAreaMapper.findList(name, glkeshi, enabled);
+    }
+
+    /**
+     * 根据ID查询病区
+     */
+    @Cacheable(value = CacheConstants.CACHE_WARD_AREA, key = "'id:' + #id")
+    public WardArea findById(Long id) {
+        return wardAreaMapper.findById(id);
+    }
+
+    /**
+     * 保存病区
+     */
+    @CacheEvict(value = CacheConstants.CACHE_WARD_AREA, allEntries = true)
+    public void save(WardArea wardArea) {
+        // 如果外部代码为空,默认等于病区代码
+        if (wardArea.getOutCode() == null || wardArea.getOutCode().isEmpty()) {
+            wardArea.setOutCode(wardArea.getCode());
+        }
+        if (wardArea.getId() == null) {
+            wardAreaMapper.insert(wardArea);
+        } else {
+            wardAreaMapper.update(wardArea);
+        }
+    }
+
+    /**
+     * 批量设置病区代码
+     * @return 返回设置失败的信息
+     */
+    @CacheEvict(value = CacheConstants.CACHE_WARD_AREA, allEntries = true)
+    public List<String> batchSetCode(List<Long> ids, List<String> codes) {
+        List<String> errors = new ArrayList<>();
+        
+        for (int i = 0; i < ids.size(); i++) {
+            Long id = ids.get(i);
+            String code = codes.get(i);
+            
+            // 格式化代码为两位数字
+            code = formatCode(code);
+            
+            // 校验代码格式
+            if (!code.matches("^\\d{2}$")) {
+                WardArea ward = wardAreaMapper.findById(id);
+                errors.add("病区[" + (ward != null ? ward.getName() : id) + "]代码格式错误,应为01~99");
+                continue;
+            }
+            
+            // 校验代码是否重复
+            WardArea existing = wardAreaMapper.findByCode(code);
+            if (existing != null && !existing.getId().equals(id)) {
+                WardArea ward = wardAreaMapper.findById(id);
+                errors.add("病区[" + (ward != null ? ward.getName() : id) + "]代码[" + code + "]已被使用");
+                continue;
+            }
+            
+            // 更新代码
+            wardAreaMapper.updateCode(id, code);
+        }
+        
+        return errors;
+    }
+
+    /**
+     * 格式化病区代码为两位数字
+     */
+    private String formatCode(String code) {
+        if (code == null || code.isEmpty()) {
+            return "00";
+        }
+        // 去除非数字字符
+        code = code.replaceAll("\\D", "");
+        if (code.isEmpty()) {
+            return "00";
+        }
+        // 取后两位
+        int num = Integer.parseInt(code);
+        if (num > 99) num = 99;
+        if (num < 0) num = 0;
+        return String.format("%02d", num);
+    }
+
+    /**
+     * 清除病区缓存
+     */
+    @CacheEvict(value = CacheConstants.CACHE_WARD_AREA, allEntries = true)
+    public void clearCache() {
+        // 清除所有病区相关缓存
+    }
+
+    /**
+     * 删除病区
+     */
+    @CacheEvict(value = CacheConstants.CACHE_WARD_AREA, allEntries = true)
+    public void deleteById(Long id) {
+        wardAreaMapper.deleteById(id);
+    }
+    
+    /**
+     * 获取所有不重复的关联科室列表
+     */
+    @Cacheable(value = CacheConstants.CACHE_OPTIONS, key = "'wardArea:glkeshi'")
+    public List<String> findAllGlkeshi() {
+        List<String> result = new ArrayList<>();
+        List<String> list = wardAreaMapper.findAllGlkeshi();
+        // 处理多个科室用逗号分隔的情况
+        for (String item : list) {
+            if (item != null && !item.isEmpty()) {
+                String[] parts = item.split(",");
+                for (String part : parts) {
+                    String trimmed = part.trim();
+                    if (!trimmed.isEmpty() && !result.contains(trimmed)) {
+                        result.add(trimmed);
+                    }
+                }
+            }
+        }
+        return result;
+    }
+}

+ 41 - 0
src/main/resources/application.properties

@@ -0,0 +1,41 @@
+# 服务器配置
+server.port=8080
+server.servlet.context-path=/shixian
+server.tomcat.uri-encoding=UTF-8
+
+# 数据库连接配置
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shixian?useUnicode=true&characterEncoding=utf-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
+spring.datasource.username=root
+spring.datasource.password=123456
+
+# HikariCP连接池配置(提升数据库访问性能)
+spring.datasource.hikari.minimum-idle=5
+spring.datasource.hikari.maximum-pool-size=20
+spring.datasource.hikari.idle-timeout=30000
+spring.datasource.hikari.connection-timeout=20000
+spring.datasource.hikari.max-lifetime=1800000
+
+# MyBatis配置
+mybatis.mapper-locations=classpath:mapper/*.xml
+mybatis.type-aliases-package=org.example.entity
+mybatis.configuration.map-underscore-to-camel-case=true
+# 生产环境建议注释掉SQL日志
+# mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
+
+# 文件上传配置
+spring.servlet.multipart.max-file-size=10MB
+spring.servlet.multipart.max-request-size=10MB
+file.upload-path=E:/xiangmu/bishe/frontend/public/img/
+
+# Redis配置
+spring.redis.host=127.0.0.1
+spring.redis.port=6379
+spring.redis.password=
+spring.redis.database=0
+# 连接池配置
+spring.redis.lettuce.pool.max-active=8
+spring.redis.lettuce.pool.max-wait=-1ms
+spring.redis.lettuce.pool.max-idle=8
+spring.redis.lettuce.pool.min-idle=0
+spring.redis.timeout=3000ms

+ 100 - 0
src/main/resources/mapper/BedMapper.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.example.mapper.BedMapper">
+
+    <resultMap id="bedResultMap" type="org.example.entity.Bed">
+        <id column="id" property="id"/>
+        <result column="code" property="code"/>
+        <result column="out_code" property="outCode"/>
+        <result column="name" property="name"/>
+        <result column="bed_type" property="bedType"/>
+        <result column="belong_dept" property="belongDept"/>
+        <result column="belong_ward" property="belongWard"/>
+        <result column="belong_room" property="belongRoom"/>
+        <result column="doctor" property="doctor"/>
+        <result column="nurse" property="nurse"/>
+        <result column="used" property="used"/>
+        <result column="sort" property="sort"/>
+        <result column="enabled" property="enabled"/>
+        <result column="remark" property="remark"/>
+    </resultMap>
+
+    <select id="findList" resultMap="bedResultMap">
+        SELECT * FROM tb_hospital_bed
+        <where>
+            <if test="code != null and code != ''">
+                AND code LIKE CONCAT('%', #{code}, '%')
+            </if>
+            <if test="belongDept != null and belongDept != ''">
+                AND belong_dept = #{belongDept}
+            </if>
+            <if test="belongWard != null and belongWard != ''">
+                AND belong_ward = #{belongWard}
+            </if>
+            <if test="belongRoom != null and belongRoom != ''">
+                AND belong_room = #{belongRoom}
+            </if>
+        </where>
+        ORDER BY sort ASC, id DESC
+    </select>
+
+    <select id="findById" resultMap="bedResultMap">
+        SELECT * FROM tb_hospital_bed WHERE id = #{id}
+    </select>
+
+    <select id="findByCodeAndWard" resultMap="bedResultMap">
+        SELECT * FROM tb_hospital_bed WHERE code = #{code} AND belong_ward = #{belongWard}
+    </select>
+
+    <insert id="insert" parameterType="org.example.entity.Bed" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO tb_hospital_bed (code, out_code, name, bed_type, belong_dept, belong_ward, belong_room, doctor, nurse, used, sort, enabled, remark)
+        VALUES (#{code}, #{outCode}, #{name}, #{bedType}, #{belongDept}, #{belongWard}, #{belongRoom}, #{doctor}, #{nurse}, #{used}, #{sort}, #{enabled}, #{remark})
+    </insert>
+
+    <update id="update" parameterType="org.example.entity.Bed">
+        UPDATE tb_hospital_bed SET
+            code = #{code},
+            out_code = #{outCode},
+            name = #{name},
+            bed_type = #{bedType},
+            belong_dept = #{belongDept},
+            belong_ward = #{belongWard},
+            belong_room = #{belongRoom},
+            doctor = #{doctor},
+            nurse = #{nurse},
+            used = #{used},
+            sort = #{sort},
+            enabled = #{enabled},
+            remark = #{remark}
+        WHERE id = #{id}
+    </update>
+
+    <update id="updateCode">
+        UPDATE tb_hospital_bed SET code = #{code} WHERE id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        DELETE FROM tb_hospital_bed WHERE id = #{id}
+    </delete>
+
+    <select id="findAllBelongDept" resultType="java.lang.String">
+        SELECT DISTINCT belong_dept FROM tb_hospital_bed WHERE belong_dept IS NOT NULL AND belong_dept != ''
+    </select>
+
+    <select id="findWardsByDept" resultType="java.lang.String">
+        SELECT DISTINCT belong_ward FROM tb_hospital_bed 
+        WHERE belong_ward IS NOT NULL AND belong_ward != ''
+        <if test="belongDept != null and belongDept != ''">
+            AND belong_dept = #{belongDept}
+        </if>
+    </select>
+
+    <select id="findRoomsByWard" resultType="java.lang.String">
+        SELECT DISTINCT belong_room FROM tb_hospital_bed 
+        WHERE belong_room IS NOT NULL AND belong_room != ''
+        <if test="belongWard != null and belongWard != ''">
+            AND belong_ward = #{belongWard}
+        </if>
+    </select>
+
+</mapper>

+ 58 - 0
src/main/resources/mapper/DepartmentMapper.xml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.example.mapper.DepartmentMapper">
+
+    <resultMap id="deptResultMap" type="org.example.entity.Department">
+        <id column="id" property="id"/>
+        <result column="code" property="code"/>
+        <result column="out_code" property="outCode"/>
+        <result column="name" property="name"/>
+        <result column="address" property="address"/>
+        <result column="telephone" property="telephone"/>
+        <result column="director" property="director"/>
+        <result column="introduction" property="introduction"/>
+        <result column="enabled" property="enabled"/>
+        <result column="remark" property="remark"/>
+    </resultMap>
+
+    <select id="findList" resultMap="deptResultMap">
+        SELECT * FROM tb_hospital_dept
+        <where>
+            <if test="name != null and name != ''">
+                AND name LIKE CONCAT('%', #{name}, '%')
+            </if>
+            <if test="enabled != null">
+                AND enabled = #{enabled}
+            </if>
+        </where>
+        ORDER BY id DESC
+    </select>
+
+    <select id="findById" resultMap="deptResultMap">
+        SELECT * FROM tb_hospital_dept WHERE id = #{id}
+    </select>
+
+    <insert id="insert" parameterType="org.example.entity.Department" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO tb_hospital_dept (code, out_code, name, address, telephone, director, introduction, enabled, remark)
+        VALUES (#{code}, #{outCode}, #{name}, #{address}, #{telephone}, #{director}, #{introduction}, #{enabled}, #{remark})
+    </insert>
+
+    <update id="update" parameterType="org.example.entity.Department">
+        UPDATE tb_hospital_dept SET
+            code = #{code},
+            out_code = #{outCode},
+            name = #{name},
+            address = #{address},
+            telephone = #{telephone},
+            director = #{director},
+            introduction = #{introduction},
+            enabled = #{enabled},
+            remark = #{remark}
+        WHERE id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        DELETE FROM tb_hospital_dept WHERE id = #{id}
+    </delete>
+
+</mapper>

+ 32 - 0
src/main/resources/mapper/HospitalMapper.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.example.mapper.HospitalMapper">
+
+    <resultMap id="hospitalResultMap" type="org.example.entity.Hospital">
+        <result column="id" property="name"/>
+        <result column="logo" property="logo"/>
+        <result column="dizhi" property="address"/>
+        <result column="dianhua" property="phone"/>
+        <result column="jieshao" property="introduction"/>
+    </resultMap>
+
+    <select id="getHospital" resultMap="hospitalResultMap">
+        SELECT * FROM yiyuangaikuang LIMIT 1
+    </select>
+
+    <insert id="insert" parameterType="org.example.entity.Hospital">
+        INSERT INTO yiyuangaikuang (id, logo, dizhi, dianhua, jieshao)
+        VALUES (#{name}, #{logo}, #{address}, #{phone}, #{introduction})
+    </insert>
+
+    <update id="update">
+        UPDATE yiyuangaikuang SET 
+            id = #{hospital.name},
+            logo = #{hospital.logo},
+            dizhi = #{hospital.address},
+            dianhua = #{hospital.phone},
+            jieshao = #{hospital.introduction}
+        WHERE id = #{oldName}
+    </update>
+
+</mapper>

+ 51 - 0
src/main/resources/mapper/PatientChargeMapper.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.example.mapper.PatientChargeMapper">
+
+    <resultMap id="chargeResultMap" type="org.example.entity.PatientCharge">
+        <id column="id" property="id"/>
+        <result column="patient_id" property="patientId"/>
+        <result column="charge_date" property="chargeDate"/>
+        <result column="item_name" property="itemName"/>
+        <result column="amount" property="amount"/>
+        <result column="quantity" property="quantity"/>
+        <result column="unit" property="unit"/>
+        <result column="subtotal" property="subtotal"/>
+        <result column="remark" property="remark"/>
+        <result column="is_paid" property="isPaid"/>
+        <result column="pay_time" property="payTime"/>
+    </resultMap>
+
+    <select id="findByPatientId" resultMap="chargeResultMap">
+        SELECT * FROM tb_patient_charges WHERE patient_id = #{patientId} ORDER BY charge_date DESC
+    </select>
+
+    <select id="findById" resultMap="chargeResultMap">
+        SELECT * FROM tb_patient_charges WHERE id = #{id}
+    </select>
+
+    <insert id="insert" parameterType="org.example.entity.PatientCharge" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO tb_patient_charges (patient_id, charge_date, item_name, amount, quantity, unit, subtotal, remark, is_paid, pay_time)
+        VALUES (#{patientId}, #{chargeDate}, #{itemName}, #{amount}, #{quantity}, #{unit}, #{subtotal}, #{remark}, #{isPaid}, #{payTime})
+    </insert>
+
+    <update id="update" parameterType="org.example.entity.PatientCharge">
+        UPDATE tb_patient_charges SET
+            patient_id = #{patientId},
+            charge_date = #{chargeDate},
+            item_name = #{itemName},
+            amount = #{amount},
+            quantity = #{quantity},
+            unit = #{unit},
+            subtotal = #{subtotal},
+            remark = #{remark},
+            is_paid = #{isPaid},
+            pay_time = #{payTime}
+        WHERE id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        DELETE FROM tb_patient_charges WHERE id = #{id}
+    </delete>
+
+</mapper>

+ 51 - 0
src/main/resources/mapper/PatientDiagnosisMapper.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.example.mapper.PatientDiagnosisMapper">
+
+    <resultMap id="diagnosisResultMap" type="org.example.entity.PatientDiagnosis">
+        <id column="id" property="id"/>
+        <result column="patient_id" property="patientId"/>
+        <result column="id_card" property="idCard"/>
+        <result column="diagnosis_type" property="diagnoseType"/>
+        <result column="diagnosis_time" property="diagnoseTime"/>
+        <result column="diagnosis_desc" property="diagnoseDesc"/>
+        <result column="diagnosis_status" property="diagnoseStatus"/>
+        <result column="diagnosis_doctor" property="diagnoseDoctor"/>
+        <result column="remark" property="remark"/>
+    </resultMap>
+
+    <select id="findByPatientId" resultMap="diagnosisResultMap">
+        SELECT * FROM tb_patient_diagnosis WHERE patient_id = #{patientId} ORDER BY diagnosis_time DESC
+    </select>
+
+    <select id="findByIdCard" resultMap="diagnosisResultMap">
+        SELECT * FROM tb_patient_diagnosis WHERE id_card = #{idCard} ORDER BY diagnosis_time DESC
+    </select>
+
+    <select id="findById" resultMap="diagnosisResultMap">
+        SELECT * FROM tb_patient_diagnosis WHERE id = #{id}
+    </select>
+
+    <insert id="insert" parameterType="org.example.entity.PatientDiagnosis" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO tb_patient_diagnosis (patient_id, id_card, diagnosis_type, diagnosis_time, diagnosis_desc, diagnosis_status, diagnosis_doctor, remark)
+        VALUES (#{patientId}, #{idCard}, #{diagnoseType}, #{diagnoseTime}, #{diagnoseDesc}, #{diagnoseStatus}, #{diagnoseDoctor}, #{remark})
+    </insert>
+
+    <update id="update" parameterType="org.example.entity.PatientDiagnosis">
+        UPDATE tb_patient_diagnosis SET
+            patient_id = #{patientId},
+            id_card = #{idCard},
+            diagnosis_type = #{diagnoseType},
+            diagnosis_time = #{diagnoseTime},
+            diagnosis_desc = #{diagnoseDesc},
+            diagnosis_status = #{diagnoseStatus},
+            diagnosis_doctor = #{diagnoseDoctor},
+            remark = #{remark}
+        WHERE id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        DELETE FROM tb_patient_diagnosis WHERE id = #{id}
+    </delete>
+
+</mapper>

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels