3 Incheckningar 37768042f7 ... 428c521fb2

Upphovsman SHA1 Meddelande Datum
  vothin 428c521fb2 Merge branch 'develop' into feature/1.5.3-update-20250331 2 månader sedan
  wuyunfeng 512aab6b3a Merge branch 'develop' of http://git.wdklian.com/allen/ncs_ui into develop 3 månader sedan
  wuyunfeng 74f7ed0e4b 超级管理员登录,新增科室授权界面和服务器授权界面 3 månader sedan

+ 37 - 2
languages/en.js

@@ -1013,7 +1013,7 @@ module.exports = {
     visitation: 'Visitor',
     transferDevice: 'Transfer box',
     lcdDevice: 'LCD corridor screen',
-    entraceguard_device: 'Access control APP',
+    entraceguard_device: 'Entrance Guard Device',
     organizationAdd: 'New Organization',
     boardShowEmptyBed: 'The board shows empty beds',
     iotProductId: 'NB Device Product ID',
@@ -1036,7 +1036,42 @@ module.exports = {
     convenientServiceEnabled: 'Convenient service',
     ledServiceEnabled: 'Server control LED',
     autoPositionEnabled: 'Enable automatic positioning',
-    boolDisplayNcTitle: 'Enable nursing title display'
+    boolDisplayNcTitle: 'Enable nursing title display',
+    departmentLicense: 'Department License',
+    licenseInfo: 'License Information',
+    licenseType: 'License Type',
+    serverLicense: 'Server License',
+    customerName: 'Customer Name',
+    createTime: 'Create Time',
+    effectiveTime: 'Effective Time',
+    expireTime: 'Expiration Time',
+    licenseStatus: 'License Status',
+    description: 'Description',
+    noLicense: 'No License Information',
+    nolimit: 'Permanent',
+    validLicense: 'Valid',
+    expiredLicense: 'Expired',
+    licenseExpiringIn: 'Expiring ({days} days left)',
+    dragZipOrClick: 'Drag the license ZIP file here, or click to upload',
+    zipTip: 'Only ZIP archives containing license file (.lic) and public key file (.key) are allowed, and the file size should not exceed 10MB',
+    onlyZipAllowed: 'Only ZIP files are allowed!',
+    fileTooLarge: 'File size should not exceed 10MB!',
+    uploadSuccess: 'License file uploaded successfully',
+    uploadError: 'Failed to upload license file',
+    fetchLicenseError: 'Failed to fetch license information',
+    hardwareInfo: 'Hardware Fingerprint Information',
+    fingerprint: 'Hardware Fingerprint',
+    envInfo: 'Environment Info',
+    updateTime: 'Update Time',
+    departmentId: 'Department ID',
+    dockerEnv: 'Docker Container Environment',
+    physicalEnv: 'Physical/Virtual Machine Environment',
+    noFingerprint: 'No fingerprint available',
+    copy: 'Copy',
+    copySuccess: 'Copied successfully',
+    getFingerprint: 'Get Hardware Fingerprint',
+    refreshFingerprint: 'Refresh Fingerprint',
+    fetchFingerprintError: 'Failed to fetch hardware fingerprint'
   },
   role: {
     roleName: 'Role name',

+ 36 - 1
languages/es.js

@@ -1036,7 +1036,42 @@ module.exports = {
     convenientServiceEnabled: 'Servicios convenientes',
     ledServiceEnabled: 'Control del servidor LED',
     autoPositionEnabled: 'Activar posicionamiento automático',
-    boolDisplayNcTitle: 'Abrir pantalla de títulos de enfermería'
+    boolDisplayNcTitle: 'Abrir pantalla de títulos de enfermería',
+    departmentLicense: 'Licencia del Departamento',
+    licenseInfo: 'Información de Licencia',
+    licenseType: 'Tipo de Licencia',
+    serverLicense: 'Licencia del Servidor',
+    customerName: 'Nombre del Cliente',
+    createTime: 'Tiempo de Creación',
+    effectiveTime: 'Tiempo Efectivo',
+    expireTime: 'Tiempo de Expiración',
+    licenseStatus: 'Estado de la Licencia',
+    description: 'Descripción',
+    noLicense: 'Sin Información de Licencia',
+    nolimit: 'Permanente',
+    validLicense: 'Válido',
+    expiredLicense: 'Expirado',
+    licenseExpiringIn: 'Expirando (quedan {days} días)',
+    dragZipOrClick: 'Arrastre el archivo ZIP de licencia aquí, o haga clic para cargar',
+    zipTip: 'Solo se permiten archivos ZIP que contengan archivo de licencia (.lic) y archivo de clave pública (.key), y el tamaño del archivo no debe exceder 10MB',
+    onlyZipAllowed: '¡Solo se permiten archivos ZIP!',
+    fileTooLarge: '¡El tamaño del archivo no debe exceder 10MB!',
+    uploadSuccess: 'Archivo de licencia cargado con éxito',
+    uploadError: 'Error al cargar el archivo de licencia',
+    fetchLicenseError: 'Error al obtener información de licencia',
+    hardwareInfo: 'Información de Huella de Hardware',
+    fingerprint: 'Huella de Hardware',
+    envInfo: 'Información del Entorno',
+    updateTime: 'Tiempo de Actualización',
+    departmentId: 'ID del Departamento',
+    dockerEnv: 'Entorno de Contenedor Docker',
+    physicalEnv: 'Entorno de Máquina Física/Virtual',
+    noFingerprint: 'No hay huella disponible',
+    copy: 'Copiar',
+    copySuccess: 'Copiado con éxito',
+    getFingerprint: 'Obtener Huella de Hardware',
+    refreshFingerprint: 'Actualizar Huella',
+    fetchFingerprintError: 'Error al obtener huella de hardware'
   },
   role: {
     roleName: 'Nombre del rol',

+ 60 - 2
languages/ru-RU.js

@@ -185,6 +185,8 @@ module.exports = {
     uploadFileMsg: 'Перетащите файл сюда или',
     uploadFileMsg2: 'Нажмите, чтобы загрузить',
     uploadFileMsg3: 'Можно загружать только файлы mp3 или flac',
+    uploadFileMsg4: 'Размер загружаемого вложения не может превышать 50 МБ!',
+    uploadFileMsg5: 'Загружать только APK-файлы',
     uploadFileMsg4: 'Размер загружаемого вложения не может превышать 50 МБ!',
     uploadFileMsg5: 'Загружать только файлы APK/IMG',
     uploadFileName: 'Имя файла',
@@ -898,6 +900,13 @@ module.exports = {
     shopFullName: 'Полное название организации',
     shopFullNameMsg: 'Необходимо заполнить полное название организации',
     inputShopFullName: 'Введите полное название организации',
+    hisCode: 'ID HIS системы',
+    attacHisCodes: 'Дополнительный логотип HIS',
+    inputHisCode: 'Пожалуйста, введите HIS идентификатор системы',
+    sharedBedHisCodes: 'Идентификатор отделения совместного размещения',
+    distinguishSharedBeds: 'Различают пациентов',
+    inputSharedBedHisCodes: 'Введите общую кровать в этом отделе, идентификатор отделения HIS, несколько отделов разделены запятой ',
+    inputAttacHisCode: 'Введите дополнительный идентификатор отдела HIS, несколько отделов разделены запятыми',
     hisCode: 'Идентификатор системы HIS',
     attacHisCodes: 'Прикрепить идентификаторы системы HIS',
     inputHisCode: 'Пожалуйста, введите идентификатор системы HIS',
@@ -1036,6 +1045,42 @@ module.exports = {
     convenientServiceEnabled: 'Удобное обслуживание',
     ledServiceEnabled: 'Светодиод управления сервером',
     autoPositionEnabled: 'Включить автоматическое позиционирование',
+    boolDisplayNcTitle: 'Открыть заголовок Уход Показать',
+    departmentLicense: 'Лицензия отдела',
+    licenseInfo: 'Информация о лицензии',
+    licenseType: 'Тип лицензии',
+    serverLicense: 'Серверная лицензия',
+    customerName: 'Имя клиента',
+    createTime: 'Время создания',
+    effectiveTime: 'Время вступления в силу',
+    expireTime: 'Срок истечения',
+    licenseStatus: 'Статус лицензии',
+    description: 'Описание',
+    noLicense: 'Нет информации о лицензии',
+    nolimit: 'Бессрочно',
+    validLicense: 'Действительна',
+    expiredLicense: 'Истекла',
+    licenseExpiringIn: 'Истекает (осталось {days} дней)',
+    dragZipOrClick: 'Перетащите ZIP-файл лицензии сюда или нажмите для загрузки',
+    zipTip: 'Разрешены только ZIP-архивы, содержащие файл лицензии (.lic) и файл открытого ключа (.key), размер файла не должен превышать 10 МБ',
+    onlyZipAllowed: 'Разрешены только ZIP-файлы!',
+    fileTooLarge: 'Размер файла не должен превышать 10 МБ!',
+    uploadSuccess: 'Файл лицензии успешно загружен',
+    uploadError: 'Не удалось загрузить файл лицензии',
+    fetchLicenseError: 'Не удалось получить информацию о лицензии',
+    hardwareInfo: 'Информация об аппаратном отпечатке',
+    fingerprint: 'Аппаратный отпечаток',
+    envInfo: 'Информация о среде',
+    updateTime: 'Время обновления',
+    departmentId: 'ID отдела',
+    dockerEnv: 'Среда контейнера Docker',
+    physicalEnv: 'Среда физической/виртуальной машины',
+    noFingerprint: 'Отпечаток недоступен',
+    copy: 'Копировать',
+    copySuccess: 'Скопировано успешно',
+    getFingerprint: 'Получить аппаратный отпечаток',
+    refreshFingerprint: 'Обновить отпечаток',
+    fetchFingerprintError: 'Не удалось получить аппаратный отпечаток',
     boolDisplayNcTitle: 'Включить отображение должности медсестры'
   },
   role: {
@@ -1453,6 +1498,13 @@ module.exports = {
     isEnable: 'Включено?'
   },
   boarderDesign: {
+    textDisplayModuleTitle: 'Показать содержимое',
+    textDisplay2ModuleTitle: ' Настройка Показать',
+    bedGridCellModuleTitle: 'Кровать ячейки',
+    bedGridModuleTitle: 'Коечный дворец',
+    rowContainerModuleTitle: 'Строчный контейнер',
+    columnContainerModuleTitle: 'Столбцы',
+    richTextModuleTitle: 'Богатый текст',
     textDisplayModuleTitle: 'Блок отображения контента',
     textDisplay2ModuleTitle: ' Пользовательский элемент отображения',
     bedGridCellModuleTitle: 'Ячейка койки',
@@ -1468,6 +1520,12 @@ module.exports = {
     componentConfig: 'Конфигурация компонента',
     borderColor: 'Цвет границы',
     borderWidth: 'Ширина границы',
+    borderTopWidth: 'Ширина сверху ',
+    borderRighhWidth: 'Ширина справа',
+    borderBottomWidth: 'Нижняя ширина',
+    borderLeftWidth: 'Ширина левого края',
+    borderRadius: 'Граничный радиус',
+    backgroundColor: 'Цвет фона',
     borderTopWidth: 'Ширина верхней границы',
     borderRighhWidth: 'Ширина правой границы',
     borderBottomWidth: 'Ширина нижней границы',
@@ -1546,8 +1604,8 @@ module.exports = {
     bedNo: 'Номер койки',
     bedName: 'Название кровати',
     patientName: 'Имя пациента',
-    patientSex: 'Пол пациента ',
-    patientAge: 'Возраст пациентов ',
+    patientSex: 'Пол пациента ',
+    patientAge: 'Возраст пациентов ',
     chargeDoctor: 'Ответственный врач',
     chargeNurse: 'Ответственная медсестра',
     inDate: 'Дата госпитализации',

+ 36 - 1
languages/zh-CN.js

@@ -1036,7 +1036,42 @@ module.exports = {
     convenientServiceEnabled: '便民服务',
     ledServiceEnabled: '服务端控制点阵屏',
     autoPositionEnabled: '自动定位',
-    boolDisplayNcTitle: '护理标题显示'
+    boolDisplayNcTitle: '护理标题显示',
+    departmentLicense: '科室授权',
+    licenseInfo: '授权信息',
+    licenseType: '授权类型',
+    serverLicense: '服务器授权',
+    customerName: '客户名称',
+    createTime: '创建时间',
+    effectiveTime: '生效时间',
+    expireTime: '过期时间',
+    licenseStatus: '授权状态',
+    description: '描述信息',
+    noLicense: '暂无授权信息',
+    nolimit: '永久有效',
+    validLicense: '授权有效',
+    expiredLicense: '授权已过期',
+    licenseExpiringIn: '即将过期(剩余{days}天)',
+    dragZipOrClick: '将授权ZIP文件拖到此处,或点击上传',
+    zipTip: '只能上传包含授权文件(.lic)和公钥文件(.key)的ZIP压缩包,且不超过10MB',
+    onlyZipAllowed: '只能上传ZIP文件!',
+    fileTooLarge: '文件大小不能超过10MB!',
+    uploadSuccess: '授权文件上传成功',
+    uploadError: '授权文件上传失败',
+    fetchLicenseError: '获取授权信息失败',
+    hardwareInfo: '硬件指纹信息',
+    fingerprint: '硬件指纹',
+    envInfo: '环境信息',
+    updateTime: '更新时间',
+    departmentId: '科室ID',
+    dockerEnv: 'Docker容器环境',
+    physicalEnv: '物理/虚拟机环境',
+    noFingerprint: '未获取到硬件指纹',
+    copy: '复制',
+    copySuccess: '复制成功',
+    getFingerprint: '获取硬件指纹',
+    refreshFingerprint: '刷新指纹',
+    fetchFingerprintError: '获取硬件指纹失败'
   },
   role: {
     roleName: '角色名称',

+ 2 - 11
public/index.html

@@ -6,17 +6,8 @@
     <meta name="renderer" content="webkit">
     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <!-- City -->
-    <link href="https://unpkg.com/@videojs/themes@1/dist/city/index.css" rel="stylesheet">
-
-    <!-- Fantasy -->
-    <link href="https://unpkg.com/@videojs/themes@1/dist/fantasy/index.css" rel="stylesheet">
-
-    <!-- Forest -->
-    <link href="https://unpkg.com/@videojs/themes@1/dist/forest/index.css" rel="stylesheet">
-
-    <!-- Sea -->
-    <link href="https://unpkg.com/@videojs/themes@1/dist/sea/index.css" rel="stylesheet">
+    <!-- videojs主题样式 -->
+    <link href="<%= BASE_URL %>video-theme.css" rel="stylesheet">
     <script src="/domain.js" ></script>
       <!--<script src="/janus.js" ></script>-->
       <!--<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/6.4.0/adapter.min.js" ></script>-->

+ 11 - 0
public/video-theme.css

@@ -0,0 +1,11 @@
+/* City主题 */
+@import url('./video-themes/city/index.css');
+
+/* Fantasy主题 */
+@import url('./video-themes/fantasy/index.css');
+
+/* Forest主题 */
+@import url('./video-themes/forest/index.css');
+
+/* Sea主题 */
+@import url('./video-themes/sea/index.css');

+ 97 - 0
src/api/license.js

@@ -0,0 +1,97 @@
+import request from '@/utils/request'
+
+/**
+ * 获取授权信息
+ * @param {String} departmentId 科室ID(可选)
+ * @returns {Promise}
+ */
+export function getLicenseInfo(departmentId) {
+  return request({
+    url: '/lc/license-info',
+    method: 'get',
+    params: {
+      departmentId
+    }
+  })
+}
+
+/**
+ * 获取科室授权信息
+ * @param {Number} partId 科室ID
+ * @returns {Promise}
+ */
+export function getPartLicenseInfo(partId) {
+  return request({
+    url: `/part/settings/${partId}`,
+    method: 'get'
+  })
+}
+
+/**
+ * 上传科室授权文件
+ * @param {Object} data 参数对象
+ * @param {File} data.zipFile 授权ZIP文件
+ * @param {String} data.departmentId 科室ID
+ * @returns {Promise}
+ */
+export function uploadPartLicense(data) {
+  const formData = new FormData()
+  formData.append('zipFile', data.zipFile)
+  formData.append('departmentId', data.departmentId)
+
+  return request({
+    url: '/lc/upload',
+    method: 'post',
+    data: formData,
+    headers: {
+      // 'Content-Type': 'multipart/form-data' // 删除这一行,让浏览器自动设置
+    },
+    transformRequest: [function(data) {
+      return data // 返回原始FormData,不进行任何转换
+    }]
+  })
+}
+
+/**
+ * 清除授权缓存
+ * @returns {Promise}
+ */
+export function clearLicenseCache() {
+  return request({
+    url: '/lc/clear-cache',
+    method: 'post'
+  })
+}
+
+/**
+ * 查询授权期限
+ * @returns {Promise}
+ */
+export function getLicenseExpire() {
+  return request({
+    url: '/lc/expire',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取服务器硬件指纹
+ * @returns {Promise}
+ */
+export function getHardwareFingerprint() {
+  return request({
+    url: '/lc/hardware-fingerprint',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取开发环境的简化硬件指纹
+ * @returns {Promise}
+ */
+export function getSimpleFingerprint() {
+  return request({
+    url: '/lc/simple-fingerprint',
+    method: 'get'
+  })
+} 

+ 13 - 1
src/router/index.js

@@ -918,7 +918,19 @@ export const adminRoutes = [
       }
     ]
   },
-
+  {
+    path: '/license',
+    component: Layout,
+    redirect: '/license/server',
+    children: [
+      {
+        path: 'server',
+        component: () => import('@/views/ncs-orginazition/server-license'),
+        name: 'server-license',
+        meta: { title: i18n.t('partInfo.serverLicense'), icon: 'el-icon-key', noCache: true }
+      }
+    ]
+  },
   { path: '*', redirect: '/404', hidden: true }
 ]
 

+ 301 - 0
src/views/ncs-orginazition/components/partLicenseUpload.vue

@@ -0,0 +1,301 @@
+<template>
+  <div class="license-upload-container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>{{ $t('partInfo.departmentLicense') }}</span>
+      </div>
+      <div v-loading="loading">
+        <div class="license-info" v-if="licenseInfo">
+          <el-descriptions :title="$t('partInfo.licenseInfo')" :column="1" border>
+            <el-descriptions-item :label="$t('partInfo.licenseType')">
+              {{ licenseType === 'server' ? $t('partInfo.serverLicense') : $t('partInfo.departmentLicense') }}
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.customerName')">{{ licenseInfo.customerName || '-' }}</el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.createTime')">{{ formatTimestamp(licenseInfo.createTime) || '-' }}</el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.effectiveTime')">{{ formatTimestamp(licenseInfo.effectiveTime) || '-' }}</el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.expireTime')">
+              <span v-if="licenseInfo.timeout === 'nolimit' || licenseInfo.expireTime === '永久有效'">{{ $t('partInfo.nolimit') }}</span>
+              <span v-else>{{ formatTimestamp(licenseInfo.expireTime || licenseInfo.timeout * 1000) || '-' }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.licenseStatus')">
+              <el-tag :type="isValid ? 'success' : 'danger'">
+                {{ isValid ? $t('partInfo.validLicense') : $t('partInfo.expiredLicense') }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.description')">{{ licenseInfo.description || '-' }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
+        <div v-else class="empty-license">
+          <el-empty :description="$t('partInfo.noLicense')"></el-empty>
+        </div>
+        
+        <!-- 硬件指纹信息区域 -->
+        <div class="hardware-info" v-if="hardwareInfo">
+          <el-divider content-position="left">{{ $t('partInfo.hardwareInfo') }}</el-divider>
+          <el-descriptions :column="1" border>
+            <el-descriptions-item :label="$t('partInfo.departmentId')">
+              {{ partId }}
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.fingerprint')">
+              <el-input 
+                type="text" 
+                v-model="hardwareInfo.fingerprint" 
+                readonly
+                :placeholder="$t('partInfo.noFingerprint')"
+              >
+                <el-button slot="append" @click="copyFingerprint" type="primary">
+                  <i class="el-icon-document-copy"></i> {{ $t('partInfo.copy') }}
+                </el-button>
+              </el-input>
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.envInfo')">
+              <el-tag v-if="hardwareInfo.isDocker" type="warning">{{ $t('partInfo.dockerEnv') }}</el-tag>
+              <el-tag v-else type="success">{{ $t('partInfo.physicalEnv') }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.updateTime')">
+              {{ formatTimestamp(hardwareInfo.timestamp) || '-' }}
+            </el-descriptions-item>
+          </el-descriptions>
+          <div class="refresh-button">
+            <el-button type="primary" size="small" @click="fetchHardwareFingerprint" icon="el-icon-refresh">
+              {{ $t('partInfo.refreshFingerprint') }}
+            </el-button>
+          </div>
+        </div>
+        <div v-else class="empty-hardware">
+          <el-button type="primary" @click="fetchHardwareFingerprint" icon="el-icon-refresh">
+            {{ $t('partInfo.getFingerprint') }}
+          </el-button>
+        </div>
+      </div>
+
+      <div class="upload-section">
+        <div class="upload-container">
+          <el-upload
+            class="upload-zip"
+            drag
+            :action="`${uploadServer}/lc/upload?departmentId=${partId}`"
+            :headers="uploadHeaders"
+            :show-file-list="false"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :before-upload="beforeUpload"
+            accept=".zip"
+            name="zipFile"
+          >
+            <i class="el-icon-upload"></i>
+            <div class="el-upload__text">
+              {{ $t('partInfo.dragZipOrClick') }}
+            </div>
+            <div class="el-upload__tip" slot="tip">
+              {{ $t('partInfo.zipTip') }}
+            </div>
+          </el-upload>
+        </div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getLicenseInfo, getHardwareFingerprint } from '@/api/license'
+import Storage from '@/utils/storage'
+
+// 获取服务器URL
+const serverUrl = domain.serverUrl
+
+export default {
+  name: 'PartLicenseUpload',
+  props: {
+    partId: {
+      type: [Number, String],
+      required: true
+    }
+  },
+  data() {
+    return {
+      licenseInfo: null,
+      licenseType: 'part',
+      isValid: false,
+      loading: false,
+      uploadServer: serverUrl,
+      uploadHeaders: {
+        'Authorization': Storage.getItem('calling_access_token'),
+        'uuid': Storage.getItem('calling_uuid')
+      },
+      hardwareInfo: null // 存储硬件指纹信息
+    }
+  },
+  mounted() {
+    this.fetchLicenseInfo()
+    this.fetchHardwareFingerprint()
+  },
+  methods: {
+    // 获取科室授权信息
+    fetchLicenseInfo() {
+      this.loading = true
+      getLicenseInfo(this.partId.toString()).then(response => {
+        if (response && response.licenseInfo) {
+          this.licenseInfo = response.licenseInfo
+          this.licenseType = response.licenseType
+          this.isValid = response.valid
+        } else {
+          this.licenseInfo = null
+          this.isValid = false
+        }
+      }).catch(error => {
+        console.error('获取授权信息失败', error)
+        this.$message.error(this.$t('partInfo.fetchLicenseError'))
+        this.licenseInfo = null
+        this.isValid = false
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    
+    // 获取硬件指纹信息
+    fetchHardwareFingerprint() {
+      this.loading = true
+      getHardwareFingerprint().then(response => {
+        if (response && response.success) {
+          this.hardwareInfo = {
+            fingerprint: response.fingerprint,
+            isDocker: response.isDocker,
+            timestamp: response.timestamp
+          }
+        } else {
+          this.$message.warning(this.$t('partInfo.noFingerprint'))
+          this.hardwareInfo = null
+        }
+      }).catch(error => {
+        console.error('获取硬件指纹失败', error)
+        this.$message.error(this.$t('partInfo.fetchFingerprintError'))
+        this.hardwareInfo = null
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    
+    // 复制硬件指纹到剪贴板
+    copyFingerprint() {
+      if (this.hardwareInfo && this.hardwareInfo.fingerprint) {
+        const input = document.createElement('input')
+        input.value = this.hardwareInfo.fingerprint
+        document.body.appendChild(input)
+        input.select()
+        document.execCommand('copy')
+        document.body.removeChild(input)
+        this.$message.success(this.$t('partInfo.copySuccess'))
+      } else {
+        this.$message.warning(this.$t('partInfo.noFingerprint'))
+      }
+    },
+    
+    // 格式化时间戳
+    formatTimestamp(timestamp) {
+      if (!timestamp) return '-'
+      if (typeof timestamp === 'string' && !isNaN(Number(timestamp))) {
+        timestamp = Number(timestamp)
+      }
+      
+      try {
+        // 如果是字符串且不能转换为数字,直接返回
+        if (typeof timestamp === 'string') {
+          return timestamp
+        }
+        
+        const date = new Date(timestamp)
+        return date.toLocaleString()
+      } catch (e) {
+        return '-'
+      }
+    },
+    // 上传成功处理
+    handleUploadSuccess(response) {
+      if (response && response.success) {
+        this.$message.success(this.$t('partInfo.uploadSuccess'))
+        // 刷新授权信息
+        this.fetchLicenseInfo()
+      } else {
+        this.$message.error(response.message || this.$t('partInfo.uploadError'))
+      }
+    },
+    // 上传失败处理
+    handleUploadError(error) {
+      console.error('上传授权文件失败', error)
+      let errorMessage = this.$t('partInfo.uploadError')
+      
+      if (error.message) {
+        errorMessage = error.message
+      }
+      
+      this.$message.error(errorMessage)
+    },
+    // 上传前验证
+    beforeUpload(file) {
+      const isZip = file.type === 'application/zip' || 
+                    file.type === 'application/x-zip-compressed' || 
+                    file.name.toLowerCase().endsWith('.zip')
+      
+      if (!isZip) {
+        this.$message.error(this.$t('partInfo.onlyZipAllowed'))
+        return false
+      }
+      
+      const isLessThan10M = file.size / 1024 / 1024 < 10
+      
+      if (!isLessThan10M) {
+        this.$message.error(this.$t('partInfo.fileTooLarge'))
+        return false
+      }
+      
+      return true
+    }
+  }
+}
+</script>
+
+<style scoped>
+.license-upload-container {
+  padding: 20px;
+}
+
+.license-info {
+  margin-bottom: 30px;
+}
+
+.empty-license {
+  margin: 30px 0;
+}
+
+.hardware-info {
+  margin: 30px 0;
+}
+
+.empty-hardware {
+  margin: 20px 0;
+  display: flex;
+  justify-content: center;
+}
+
+.refresh-button {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 10px;
+}
+
+.upload-section {
+  margin-top: 30px;
+  display: flex;
+  justify-content: center;
+}
+
+.upload-container {
+  width: 80%;
+  max-width: 600px;
+}
+
+.upload-zip {
+  width: 100%;
+}
+</style> 

+ 292 - 0
src/views/ncs-orginazition/components/serverLicenseUpload.vue

@@ -0,0 +1,292 @@
+<template>
+  <div class="license-upload-container">
+    <el-card class="box-card">
+      <div slot="header" class="clearfix">
+        <span>{{ $t('partInfo.serverLicense') }}</span>
+      </div>
+      <div v-loading="loading">
+        <div class="license-info" v-if="licenseInfo">
+          <el-descriptions :title="$t('partInfo.licenseInfo')" :column="1" border>
+            <el-descriptions-item :label="$t('partInfo.licenseType')">
+              {{ $t('partInfo.serverLicense') }}
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.customerName')">{{ licenseInfo.customerName || '-' }}</el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.createTime')">{{ formatTimestamp(licenseInfo.createTime) || '-' }}</el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.effectiveTime')">{{ formatTimestamp(licenseInfo.effectiveTime) || '-' }}</el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.expireTime')">
+              <span v-if="licenseInfo.timeout === 'nolimit' || licenseInfo.expireTime === '永久有效'">{{ $t('partInfo.nolimit') }}</span>
+              <span v-else>{{ formatTimestamp(licenseInfo.expireTime || licenseInfo.timeout * 1000) || '-' }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.licenseStatus')">
+              <el-tag :type="isValid ? 'success' : 'danger'">
+                {{ isValid ? $t('partInfo.validLicense') : $t('partInfo.expiredLicense') }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.description')">{{ licenseInfo.description || '-' }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
+        <div v-else class="empty-license">
+          <el-empty :description="$t('partInfo.noLicense')"></el-empty>
+        </div>
+        
+        <!-- 硬件指纹信息区域 -->
+        <div class="hardware-info" v-if="hardwareInfo">
+          <el-divider content-position="left">{{ $t('partInfo.hardwareInfo') || '硬件指纹信息' }}</el-divider>
+          <el-descriptions :column="1" border>
+            <el-descriptions-item :label="$t('partInfo.fingerprint') || '硬件指纹'">
+              <el-input 
+                type="text" 
+                v-model="hardwareInfo.fingerprint" 
+                readonly
+                :placeholder="$t('partInfo.noFingerprint') || '未获取到硬件指纹'"
+              >
+                <el-button slot="append" @click="copyFingerprint" type="primary">
+                  <i class="el-icon-document-copy"></i> {{ $t('partInfo.copy') || '复制' }}
+                </el-button>
+              </el-input>
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.envInfo') || '环境信息'">
+              <el-tag v-if="hardwareInfo.isDocker" type="warning">{{ $t('partInfo.dockerEnv') }}</el-tag>
+              <el-tag v-else type="success">{{ $t('partInfo.physicalEnv') }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item :label="$t('partInfo.updateTime') || '更新时间'">
+              {{ formatTimestamp(hardwareInfo.timestamp) || '-' }}
+            </el-descriptions-item>
+          </el-descriptions>
+          <div class="refresh-button">
+            <el-button type="primary" size="small" @click="fetchHardwareFingerprint" icon="el-icon-refresh">
+              {{ $t('partInfo.refreshFingerprint') || '刷新指纹' }}
+            </el-button>
+          </div>
+        </div>
+        <div v-else class="empty-hardware">
+          <el-button type="primary" @click="fetchHardwareFingerprint" icon="el-icon-refresh">
+            {{ $t('partInfo.getFingerprint') || '获取硬件指纹' }}
+          </el-button>
+        </div>
+      </div>
+
+      <div class="upload-section">
+        <div class="upload-container">
+          <el-upload
+            class="upload-zip"
+            drag
+            :action="`${uploadServer}/lc/upload`"
+            :headers="uploadHeaders"
+            :show-file-list="false"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :before-upload="beforeUpload"
+            accept=".zip"
+            name="zipFile"
+          >
+            <i class="el-icon-upload"></i>
+            <div class="el-upload__text">
+              {{ $t('partInfo.dragZipOrClick') }}
+            </div>
+            <div class="el-upload__tip" slot="tip">
+              {{ $t('partInfo.zipTip') }}
+            </div>
+          </el-upload>
+        </div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getLicenseInfo, getHardwareFingerprint } from '@/api/license'
+import Storage from '@/utils/storage'
+
+// 获取服务器URL
+const serverUrl = domain.serverUrl
+
+export default {
+  name: 'ServerLicenseUpload',
+  data() {
+    return {
+      licenseInfo: null,
+      licenseType: 'server',
+      isValid: false,
+      loading: false,
+      uploadServer: serverUrl,
+      uploadHeaders: {
+        'Authorization': Storage.getItem('calling_access_token'),
+        'uuid': Storage.getItem('calling_uuid')
+      },
+      hardwareInfo: null // 存储硬件指纹信息
+    }
+  },
+  mounted() {
+    this.fetchLicenseInfo()
+    this.fetchHardwareFingerprint()
+  },
+  methods: {
+    // 获取服务器授权信息
+    fetchLicenseInfo() {
+      this.loading = true
+      getLicenseInfo().then(response => {
+        if (response && response.licenseInfo) {
+          this.licenseInfo = response.licenseInfo
+          this.licenseType = response.licenseType
+          this.isValid = response.valid
+        } else {
+          this.licenseInfo = null
+          this.isValid = false
+        }
+      }).catch(error => {
+        console.error('获取授权信息失败', error)
+        this.$message.error(this.$t('partInfo.fetchLicenseError'))
+        this.licenseInfo = null
+        this.isValid = false
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    
+    // 获取硬件指纹信息
+    fetchHardwareFingerprint() {
+      this.loading = true
+      getHardwareFingerprint().then(response => {
+        if (response && response.success) {
+          this.hardwareInfo = {
+            fingerprint: response.fingerprint,
+            isDocker: response.isDocker,
+            timestamp: response.timestamp
+          }
+        } else {
+          this.$message.warning(this.$t('partInfo.noFingerprint') || '未能获取硬件指纹')
+          this.hardwareInfo = null
+        }
+      }).catch(error => {
+        console.error('获取硬件指纹失败', error)
+        this.$message.error(this.$t('partInfo.fetchFingerprintError') || '获取硬件指纹失败')
+        this.hardwareInfo = null
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    
+    // 复制硬件指纹到剪贴板
+    copyFingerprint() {
+      if (this.hardwareInfo && this.hardwareInfo.fingerprint) {
+        const input = document.createElement('input')
+        input.value = this.hardwareInfo.fingerprint
+        document.body.appendChild(input)
+        input.select()
+        document.execCommand('copy')
+        document.body.removeChild(input)
+        this.$message.success(this.$t('partInfo.copySuccess') || '复制成功')
+      } else {
+        this.$message.warning(this.$t('partInfo.noFingerprint') || '未获取到硬件指纹')
+      }
+    },
+    
+    // 格式化时间戳
+    formatTimestamp(timestamp) {
+      if (!timestamp) return '-'
+      if (typeof timestamp === 'string' && !isNaN(Number(timestamp))) {
+        timestamp = Number(timestamp)
+      }
+      
+      try {
+        // 如果是字符串且不能转换为数字,直接返回
+        if (typeof timestamp === 'string') {
+          return timestamp
+        }
+        
+        const date = new Date(timestamp)
+        return date.toLocaleString()
+      } catch (e) {
+        return '-'
+      }
+    },
+    // 上传成功处理
+    handleUploadSuccess(response) {
+      if (response && response.success) {
+        this.$message.success(this.$t('partInfo.uploadSuccess'))
+        // 刷新授权信息
+        this.fetchLicenseInfo()
+      } else {
+        this.$message.error(response.message || this.$t('partInfo.uploadError'))
+      }
+    },
+    // 上传失败处理
+    handleUploadError(error) {
+      console.error('上传授权文件失败', error)
+      let errorMessage = this.$t('partInfo.uploadError')
+      
+      if (error.message) {
+        errorMessage = error.message
+      }
+      
+      this.$message.error(errorMessage)
+    },
+    // 上传前验证
+    beforeUpload(file) {
+      const isZip = file.type === 'application/zip' || 
+                    file.type === 'application/x-zip-compressed' || 
+                    file.name.toLowerCase().endsWith('.zip')
+      
+      if (!isZip) {
+        this.$message.error(this.$t('partInfo.onlyZipAllowed'))
+        return false
+      }
+      
+      const isLessThan10M = file.size / 1024 / 1024 < 10
+      
+      if (!isLessThan10M) {
+        this.$message.error(this.$t('partInfo.fileTooLarge'))
+        return false
+      }
+      
+      return true
+    }
+  }
+}
+</script>
+
+<style scoped>
+.license-upload-container {
+  padding: 20px;
+}
+
+.license-info {
+  margin-bottom: 30px;
+}
+
+.empty-license {
+  margin: 30px 0;
+}
+
+.hardware-info {
+  margin: 30px 0;
+}
+
+.empty-hardware {
+  margin: 20px 0;
+  display: flex;
+  justify-content: center;
+}
+
+.refresh-button {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 10px;
+}
+
+.upload-section {
+  margin-top: 30px;
+  display: flex;
+  justify-content: center;
+}
+
+.upload-container {
+  width: 80%;
+  max-width: 600px;
+}
+
+.upload-zip {
+  width: 100%;
+}
+</style> 

+ 13 - 1
src/views/ncs-orginazition/partInfoSetting.vue

@@ -11,6 +11,11 @@
       <!--          <part-user-manager :part-id="part_id" />-->
       <!--        </keep-alive>-->
       <!--      </el-tab-pane>-->
+      <el-tab-pane :label="this.$t('partInfo.departmentLicense')" name="departmentLicense">
+        <keep-alive>
+          <part-license-upload :part-id="part_id" />
+        </keep-alive>
+      </el-tab-pane>
       <el-tab-pane :label="this.$t('partInfo.nurse')" name="nurse">
         <keep-alive>
           <app-version-manager :part-id="part_id" :device-type="1" />
@@ -119,9 +124,16 @@
 import PartInfoEdit from './components/partInfoEdit'
 import PartUserManager from './components/partUserManager'
 import AppVersionManager from './components/AppVersionManager'
+import PartLicenseUpload from './components/partLicenseUpload'
+
 export default {
   name: 'PartInfoSetting',
-  components: { AppVersionManager, PartUserManager, PartInfoEdit },
+  components: { 
+    AppVersionManager, 
+    PartUserManager, 
+    PartInfoEdit,
+    PartLicenseUpload
+  },
   data() {
     return {
       part_id: Number(this.$route.params.id),

+ 19 - 0
src/views/ncs-orginazition/server-license.vue

@@ -0,0 +1,19 @@
+<template>
+  <div>
+    <server-license-upload />
+  </div>
+</template>
+
+<script>
+import ServerLicenseUpload from './components/serverLicenseUpload'
+
+export default {
+  name: 'ServerLicense',
+  components: { 
+    ServerLicenseUpload
+  }
+}
+</script>
+
+<style scoped>
+</style>