Prechádzať zdrojové kódy

fix(@six/health-remedy):上传治疗图片供用户选择,治疗时的时候修改备注或者治疗图片时治疗时间不重新计时

张田田 1 deň pred
rodič
commit
720210b400

+ 241 - 0
apps/health-remedy/src/adapter/component/AvatarTwo.vue

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

+ 3 - 0
apps/health-remedy/src/adapter/component/index.ts

@@ -61,6 +61,7 @@ const TreeSelect = defineAsyncComponent(
 );
 const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
 const Avatar = defineAsyncComponent(() => import('./Avatar.vue'));
+const AvatarTwo = defineAsyncComponent(() => import('./AvatarTwo.vue'));
 
 const withDefaultPlaceholder = <T extends Component>(
   component: T,
@@ -102,6 +103,7 @@ export type ComponentType =
   | 'ApiTreeSelect'
   | 'AutoComplete'
   | 'Avatar'
+  | 'AvatarTwo'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
@@ -193,6 +195,7 @@ async function initComponentAdapter() {
     TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
     Upload,
     Avatar,
+    AvatarTwo,
   };
 
   // 将组件注册到全局共享状态中

+ 12 - 3
apps/health-remedy/src/api/index.ts

@@ -27,12 +27,17 @@ export const http = createRequestClient({
       Array.isArray(result.data?.Items)
     ) {
       const {
-        TotalPageCount: total,
+        // TotalPageCount: total,
+        TotalRecordCount: total,
         PageIndex: page,
         PageSize: size,
         Items: items,
       } = result.data;
-      result.data = { total, items, data: { page, size, total } };
+      result.data = {
+        total,
+        items,
+        data: { page, size, total },
+      };
     }
 
     /* 额外处理登录接口 */
@@ -64,7 +69,11 @@ export type TransformData<T = any> = Recordable<T>;
 export interface TransformList<T = TransformData> {
   total: number;
   items: T[];
-  data?: { page: number; size: number; total: number };
+  data?: {
+    page: number;
+    size: number;
+    total: number;
+  };
 }
 
 export interface TransformBody<T> {

+ 4 - 3
apps/health-remedy/src/api/method/operate.ts

@@ -1,12 +1,11 @@
 import type { TransformData, TransformList, TransformRecord } from '#/api';
 
 import { http } from '#/api';
-import { fromRecord } from '#/api/model/operate';
+import { fromRecord, toRecord } from '#/api/model/operate';
 
 export namespace OperateModel {
   export interface Record extends TransformRecord {
     [key: string]: any;
-
     id: string;
     name: string;
     itemName: string;
@@ -23,13 +22,15 @@ export namespace OperateModel {
     treatmentTime: string;
     remark?: string;
     status: 0 | 1;
+    startTime: string;
+    endTime: string;
   }
 }
 // 获取机构列表
 export function listRecordsMethod(page = 1, size = 20, query?: TransformData) {
   return http.post<TransformList<OperateModel.Record>, TransformList>(
     `/basis/operate/listPage`,
-    query,
+    toRecord(query),
     {
       params: { page, limit: size },
       transform({ items, ...data }) {

+ 6 - 3
apps/health-remedy/src/api/method/register.ts

@@ -1,7 +1,7 @@
 import type { TransformData, TransformList, TransformRecord } from '#/api';
 
 import { http } from '#/api';
-import { fromRegister } from '#/api/model/register';
+import { fromRegister, toRegister } from '#/api/model/register';
 
 export namespace RegisterModel {
   export interface Register extends TransformRecord {
@@ -32,11 +32,14 @@ export namespace RegisterModel {
 export function listRegisterMethod(page = 1, size = 20, query?: TransformData) {
   return http.post<TransformList<RegisterModel.Register>, TransformList>(
     `/basis/item/registerList`,
-    query,
+    toRegister(query),
     {
       params: { page, limit: size },
       transform({ items, ...data }) {
-        return { ...data, items: items.map((item) => fromRegister(item)) };
+        return {
+          ...data,
+          items: items.map((item) => fromRegister(item)),
+        };
       },
     },
   );

+ 2 - 0
apps/health-remedy/src/api/method/treatment.ts

@@ -37,6 +37,7 @@ export namespace TreatmentModel {
         acupointName: string;
         acuType: string;
         id: string;
+        knowledgeId: string;
       },
     ];
     behavior: string;
@@ -125,6 +126,7 @@ export function saveTreatmentMethod(
 }
 // 获取穴位详情
 export function getAcupointDetailMethod(id: string) {
+  console.log(id, '获取穴为id');
   return http.Post<any, any>(
     `/knowledge/acuPoint/detail`,
     { id },

+ 4 - 0
apps/health-remedy/src/api/model/operate.ts

@@ -21,6 +21,8 @@ export function fromRecord(data?: TransformData): OperateModel.Record {
     issueDate: data?.issueDate,
     remark: data?.remark,
     status: data?.status,
+    startTime: data?.startTime,
+    endTime: data?.endTime,
   };
 }
 
@@ -41,5 +43,7 @@ export function toRecord(data?: Partial<OperateModel.Record>): TransformData {
     remark: data?.remark,
     status: data?.status,
     acupointName: data?.acupointName,
+    startTime: data?.startTime,
+    endTime: data?.endTime,
   };
 }

+ 1 - 19
apps/health-remedy/src/views/operate/record/data.ts

@@ -29,25 +29,7 @@ export function useUserSearchFormSchema(): VbenFormSchema[] {
           $t('operate.record.startTime'),
           $t('operate.record.endTime'),
         ],
-        onChange: (_dates: any, _dateStrings: string[]) => {
-          // 时间范围字段分离逻辑在list.vue的handleValuesChange中处理
-        },
-      },
-    },
-    {
-      component: 'Input',
-      fieldName: 'startTime',
-      label: '',
-      componentProps: {
-        style: { display: 'none' },
-      },
-    },
-    {
-      component: 'Input',
-      fieldName: 'endTime',
-      label: '',
-      componentProps: {
-        style: { display: 'none' },
+        inputReadOnly: true,
       },
     },
   ];

+ 0 - 8
apps/health-remedy/src/views/operate/record/list.vue

@@ -52,14 +52,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
     rowConfig: {
       keyField: 'id',
     },
-
-    // toolbarConfig: {
-    //   custom: true,
-    //   export: false,
-    //   refresh: true,
-    //   search: true,
-    //   zoom: true,
-    // },
   } as VxeTableGridOptions<SystemModel.User>,
 });
 // 表格的操作 只处理查看功能

+ 0 - 8
apps/health-remedy/src/views/register/register/list.vue

@@ -42,14 +42,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
     rowConfig: {
       keyField: 'id',
     },
-
-    // toolbarConfig: {
-    //   custom: true,
-    //   export: false,
-    //   refresh: true,
-    //   search: true,
-    //   zoom: true,
-    // },
   } as VxeTableGridOptions<RegisterModel.Register>,
 });
 // 表格的操作 登记功能

+ 2 - 2
apps/health-remedy/src/views/register/register/modules/form.vue

@@ -183,9 +183,9 @@ function handleCancel() {
             }} -->
             {{ $t('register.register.verifySuccess') }}
           </div>
-          <div class="mb-6 text-gray-600">
+          <!-- <div class="mb-6 text-gray-600">
             {{ $t('register.register.tips') }}
-          </div>
+          </div> -->
 
           <div class="mb-4 text-left">
             <div class="mb-3 font-medium">

+ 72 - 25
apps/health-remedy/src/views/treatment/treatment/components/TreatmentDetail.vue

@@ -73,6 +73,7 @@ const acupointData = computed(() => {
     index: (index + 1) as any,
     acupointName: acupoint.acupointName,
     acuType: acupoint.acuType, // 添加 acuType 字段
+    knowledgeId: acupoint.knowledgeId,
   }));
 
   // 将穴位数据按4个一组重新组织,每行4个穴位
@@ -86,6 +87,7 @@ const acupointData = computed(() => {
         index: '',
         acupointName: '',
         acuType: '',
+        knowledgeId: '',
       });
     }
     groupedData.push({
@@ -173,6 +175,17 @@ watch(
   { immediate: false },
 );
 
+// 监听 operates 数据变化,强制刷新表格
+watch(
+  () => props.treatment?.operates,
+  () => {
+    nextTick(() => {
+      operateGridApi?.grid?.reloadData?.(operatesData.value);
+    });
+  },
+  { deep: true, immediate: false },
+);
+
 const formatCountdown = (sec: number) => {
   const m = Math.floor(sec / 60)
     .toString()
@@ -187,7 +200,7 @@ function toTsWithOffset(input?: Date | number | string) {
   if (!input) return 0;
   if (typeof input === 'number') return input;
   if (input instanceof Date) return input.getTime();
-  // 兼容 2025-10-13T15:09:26.000+0800
+
   const normalized = String(input).replace(/([+-]\d{2})(\d{2})$/, '$1:$2');
   const t = new Date(normalized).getTime();
   return Number.isFinite(t) ? t : 0;
@@ -225,8 +238,9 @@ const remainSeconds = computed(() => {
   // 如果没有父组件倒计时,则使用本地计算
   const d: any = treatmentDetail.value || {};
   const cur: any = d.currentOperate || {};
-  const totalMin = cur.treatmentTime ?? d.treatmentTime;
-  const upd = cur.updateTime ?? d.updateTime ?? cur.operateDate;
+  const totalMin = cur?.treatmentTime;
+  // const upd = cur.updateTime ?? d.updateTime ?? cur.operateDate;
+  const upd = cur?.operateDate;
   const total = Math.max(0, Math.floor(Number(totalMin || 0) * 60));
   const passed = diffSeconds(upd, nowTick.value);
   return Math.max(0, total - passed);
@@ -235,6 +249,8 @@ const remainSeconds = computed(() => {
 const treatmentStartTime = ref<null | number>(null);
 const isEndingTreatment = ref(false);
 
+// 是否编辑了治疗时间
+const isTreatmentTimeEdited = ref(false);
 // 计算是否倒计时结束
 const isTimerFinished = computed(() => {
   return isInProgress.value && remainSeconds.value <= 0;
@@ -251,7 +267,7 @@ const startSchemas: VbenFormSchema[] = [
   {
     fieldName: 'treatmentImageUrl',
     label: $t('treatment.detail.treatmentPhoto'),
-    component: 'Avatar',
+    component: 'AvatarTwo',
     componentProps: {
       accept: 'image/*',
     },
@@ -260,7 +276,13 @@ const startSchemas: VbenFormSchema[] = [
     fieldName: 'treatmentTime',
     label: $t('treatment.detail.treatmentTime'),
     component: 'InputNumber',
-    componentProps: { min: 1, placeholder: $t('treatment.detail.input') },
+    componentProps: {
+      min: 1,
+      placeholder: $t('treatment.detail.input'),
+      onChange: () => {
+        isTreatmentTimeEdited.value = true;
+      },
+    },
     suffix: $t('treatment.detail.minutes'),
     rules: 'required',
   },
@@ -280,25 +302,52 @@ const [Modal, modalApi] = useVbenModal({
     const values = await formApi.getValues();
     const minutes = Number(values.treatmentTime);
 
+    if (
+      values.treatmentImageUrl === undefined ||
+      values.treatmentImageUrl === null
+    ) {
+      values.treatmentImageUrl = '';
+    }
+
     try {
+      // 治疗中
       if (treatmentDetail.value.itemState === 1) {
         values.id = treatmentDetail.value.currentOperate.id;
+        if (!isTreatmentTimeEdited.value) {
+          // 说明治疗中的时候编辑了时间
+          values.treatmentTime = '';
+        }
       }
 
       values.recordId = treatmentDetail.value.id;
       await saveTreatmentMethod(values);
       notification.success({ message: $t('treatment.detail.saveSuccess') });
 
-      // 只有在保存成功后才设置治疗进行中状态
-      // 这样可以避免在数据未更新时立即触发倒计时结束
-      isInProgress.value = true;
-      treatmentStartTime.value = Date.now();
+      // 判断是否需要重新计时
+      const isCurrentlyInProgress = treatmentDetail.value.itemState === 1;
 
-      // 触发本地计时
-      emit('startTreatment', {
-        treatmentId: treatmentDetail.value.id,
-        minutes: Math.max(1, minutes),
-      });
+      if (!isCurrentlyInProgress) {
+        // 治疗未开始,需要重新计时
+        isInProgress.value = true;
+        treatmentStartTime.value = Date.now();
+
+        // 触发本地计时
+        emit('startTreatment', {
+          treatmentId: treatmentDetail.value.id,
+          minutes: Math.max(1, minutes),
+        });
+      } else if (isTreatmentTimeEdited.value) {
+        // 治疗进行中且治疗时间被编辑了,需要重新计时
+        isInProgress.value = true;
+        treatmentStartTime.value = Date.now();
+
+        // 触发本地计时
+        emit('startTreatment', {
+          treatmentId: treatmentDetail.value.id,
+          minutes: Math.max(1, minutes),
+        });
+      }
+      // 如果只是修改备注或照片,不触发重新计时
 
       // 触发刷新数据事件,更新标签页计数
       emit('refreshData', { treatmentId: treatmentDetail.value.id });
@@ -326,6 +375,9 @@ const [AcupointModal, acupointModalApi] = useVbenModal({
 });
 
 const handleStartTreatment = (treatment: TreatmentModel.TreatmentDetail) => {
+  // 重置编辑标记
+  isTreatmentTimeEdited.value = false;
+
   // 基础默认值
   const baseValues: Record<string, any> = {
     recordId: treatment.id,
@@ -336,6 +388,7 @@ const handleStartTreatment = (treatment: TreatmentModel.TreatmentDetail) => {
 
   // 若处于治疗中,则回填当前操作项数据,供用户修改
   const maybeCurrent = (treatmentDetail.value as any)?.currentOperate;
+  console.log(maybeCurrent, 'maybeCurrent');
   if (treatmentDetail.value?.itemState === 1 && maybeCurrent) {
     formApi.setValues({
       ...baseValues,
@@ -380,12 +433,10 @@ watch(
   () => isTimerFinished.value,
   async (finished) => {
     if (!finished) return;
-
     // 防止重复调用
     if (isEndingTreatment.value) {
       return;
     }
-
     isEndingTreatment.value = true;
     try {
       await endTreatmentMethod([treatmentDetail.value.id]);
@@ -408,12 +459,10 @@ const handleAcupointClick = async (
     case 1: {
       try {
         // 获取穴位
-        const detail = await getAcupointDetailMethod(acupoint.id);
+        const detail = await getAcupointDetailMethod(acupoint.knowledgeId);
         selectedAcupoint.value = { ...acupoint, ...detail } as any;
-      } catch {
-        notification.error({
-          message: $t('treatment.detail.getAcupointDetailFailure'),
-        });
+      } catch (error) {
+        console.error('获取穴位详情失败:', error);
       }
       break;
     }
@@ -422,10 +471,8 @@ const handleAcupointClick = async (
       try {
         const detail = await getMeridianDetailMethod(acupoint.id);
         selectedAcupoint.value = { ...acupoint, ...detail } as any;
-      } catch {
-        notification.error({
-          message: $t('treatment.detail.getMeridianDetailFailure'),
-        });
+      } catch (error) {
+        console.error('获取经络详情失败:', error);
       }
       break;
     }

+ 2 - 17
apps/health-remedy/src/views/treatment/treatment/list.vue

@@ -198,18 +198,9 @@ const refreshData = async (payload?: {
         if (treatmentIndex !== -1) {
           selectedTreatmentIndex.value = treatmentIndex;
         }
-        const updatedDetail = await buildTreatmentDetailFromBasic({
+        await buildTreatmentDetailFromBasic({
           id: targetTreatmentId,
         });
-        console.warn('刷新治疗详情后状态:', updatedDetail?.itemState);
-        console.warn(
-          'selectedTreatmentDetail状态:',
-          selectedTreatmentDetail.value?.itemState,
-        );
-        console.warn(
-          'selectedTreatment状态:',
-          selectedTreatment.value?.itemState,
-        );
       }
     }
 
@@ -233,7 +224,6 @@ const handleStartTreatment = async (payload: {
   minutes: number;
   treatmentId: string;
 }) => {
-  console.warn('handleStartTreatment被调用:', payload);
   const { treatmentId, minutes } = payload;
 
   // 停止该治疗项之前的倒计时
@@ -243,10 +233,7 @@ const handleStartTreatment = async (payload: {
 
   // 设置新的倒计时
   const seconds = minutes * 60;
-  console.warn('seconds:', seconds);
-  console.warn('treatmentCountdowns:', treatmentCountdowns.value);
   treatmentCountdowns.value[treatmentId] = seconds;
-  console.warn(`开始治疗: ${treatmentId}, 设置倒计时: ${seconds}秒`);
 
   // 启动倒计时 - 现在由子组件处理计时结束逻辑
   countdownTimers.value[treatmentId] = window.setInterval(() => {
@@ -255,7 +242,6 @@ const handleStartTreatment = async (payload: {
 
     if (currentCount > 0) {
       treatmentCountdowns.value[treatmentId] = currentCount - 1;
-      console.warn(`倒计时进行中: ${treatmentId} 剩余 ${currentCount - 1} 秒`);
     } else {
       // 倒计时结束,清理定时器
       if (countdownTimers.value[treatmentId]) {
@@ -263,7 +249,6 @@ const handleStartTreatment = async (payload: {
         delete countdownTimers.value[treatmentId];
         delete treatmentCountdowns.value[treatmentId];
       }
-      console.warn(`倒计时结束: ${treatmentId}`);
     }
   }, 1000);
 
@@ -289,7 +274,7 @@ const updateTreatmentStatus = async (
       selectedPatient.value = updatedPatient;
     }
   } catch (error) {
-    console.warn('更新患者列表失败:', error);
+    console.error('更新患者列表失败:', error);
   }
 
   // 更新当前选中的治疗项状态