Ver Fonte

Merge branch 'story-237' of ssh://121.43.162.141:10022/six.fe/health.admin into feature/test

张田田 há 1 mês atrás
pai
commit
842ecfeadf
39 ficheiros alterados com 1047 adições e 129 exclusões
  1. 36 59
      src/components/EditSupplier.vue
  2. 77 63
      src/model/order.model.ts
  3. 1 0
      src/order/DispatchOrderPanel.vue
  4. 4 0
      src/pages/index/care/conditioningRecord.vue
  5. 5 0
      src/pages/index/care/configured.vue
  6. 4 0
      src/pages/index/care/institutionService.vue
  7. 5 0
      src/pages/index/care/supplier.vue
  8. 4 0
      src/pages/index/care/systemService.vue
  9. 1 0
      src/pages/index/equipment/configured.vue
  10. 5 0
      src/pages/index/equipment/registe.vue
  11. 5 0
      src/pages/index/equipment/reportManagement.vue
  12. 5 0
      src/pages/index/follow/assessment.vue
  13. 4 0
      src/pages/index/follow/plan.vue
  14. 5 0
      src/pages/index/follow/task.vue
  15. 4 0
      src/pages/index/healthy/education.vue
  16. 4 0
      src/pages/index/order/management.vue
  17. 4 0
      src/pages/index/order/revenueSharing.vue
  18. 4 0
      src/pages/index/order/shipment.vue
  19. 4 0
      src/pages/index/patient/history.vue
  20. 5 0
      src/pages/index/system/institution.vue
  21. 4 0
      src/pages/index/system/organization.vue
  22. 4 0
      src/pages/index/system/role.vue
  23. 4 0
      src/pages/index/system/tag.vue
  24. 4 0
      src/pages/index/system/user.vue
  25. 16 1
      src/request/api/order.api.ts
  26. 4 0
      src/satisfaction/SendRecord.vue
  27. 4 0
      src/satisfaction/SurveyList.vue
  28. 4 0
      src/service/NotifyManageList.vue
  29. 4 0
      src/service/NotifyManageRecord.vue
  30. 123 0
      src/service/ReviewMediaPreview.vue
  31. 5 0
      src/service/ServiceItemsConfirm.vue
  32. 5 0
      src/service/ServiceItemsList.vue
  33. 5 0
      src/service/ServiceItemsSystem.vue
  34. 319 6
      src/service/SingleItemDetail.vue
  35. 340 0
      src/service/seeEvaluate.vue
  36. 4 0
      src/widgets/PatientCareRecordsWidget.vue
  37. 4 0
      src/widgets/PatientDiagnosisRecordsWidget.vue
  38. 4 0
      src/widgets/PatientFollowUpRecordsWidget.vue
  39. 4 0
      src/widgets/PatientHealthRecordsWidget.vue

+ 36 - 59
src/components/EditSupplier.vue

@@ -289,6 +289,17 @@ initTimeRangeValues();
 
 const formRef = ref<any>(null);
 
+// 营业时间字段(始终存在于表单中,通过样式控制显示/隐藏,避免动态增删表单项导致下拉失焦收起)
+const businessHoursField = {
+  field: 'businessTime',
+  title: '营业时间',
+  span: 24,
+  slots: {
+    title: 'businessTimeTitle',
+    default: 'businessHours',
+  },
+  className: 'business-hours-item',
+} as const;
 
 const baseFormItems = [
   {
@@ -412,6 +423,7 @@ const baseFormItems = [
     span: 24,
     itemRender: { name: 'VxeInput', props: { placeholder: '请输入', type: 'number', min: 0, max: 100 } },
   },
+  businessHoursField as any,
   // 营业状态 单选,营业、休息、停业
   {
     field: 'businessStatus',
@@ -423,44 +435,12 @@ const baseFormItems = [
   { align: 'center', span: 24, slots: { default: 'active' } },
 ];
 
-// 营业时间字段
-const businessHoursField = {
-  field: 'businessTime',
-  title: '营业时间',
-  span: 24,
-  slots: {
-    title: 'businessTimeTitle',
-    default: 'businessHours',
-  },
-} as const;
-
-// 更新表单项
-function updateFormItems() {
-  const items = [...baseFormItems];
-  const shouldShow = showBusinessHours.value;
-  // 查找分账比例和营业时间
-  const profitSharingIndex = items.findIndex(item => item.field === 'profitSharing');
-  const businessTimeIndex = items.findIndex(item => item.field === 'businessTime');
-
-  if (shouldShow) {
-    if (businessTimeIndex === -1 && profitSharingIndex !== -1) {
-      items.splice(profitSharingIndex + 1, 0, businessHoursField as any);
-    }
-  } else {
-    if (businessTimeIndex !== -1) {
-      items.splice(businessTimeIndex, 1);
-    }
-  }
-
-  formProps.items = items as any;
-}
-
 const formProps = reactive<VxeFormProps<FormModel>>({
   titleWidth: 100,
   titleAlign: 'right',
   titleColon: true,
   data: { ...props.data },
-  items: baseFormItems as any,
+  items: [...baseFormItems] as any,
   rules: {
     name: [{ required: true, message: '请输入供应商名称' }],
     detailAddress: [{ required: true, message: '请输入地址' }],
@@ -519,12 +499,12 @@ const formProps = reactive<VxeFormProps<FormModel>>({
 });
 
 // 判断是否显示营业时间(仅限"线下项目"有值时展示)
-const showBusinessHours = computed<boolean>(() => {
+const hasOfflineSelected = computed<boolean>(() => {
   const offlineCPTypes: string[] | undefined = formProps.data?.offlineCPTypes;
   return !!(offlineCPTypes && Array.isArray(offlineCPTypes) && offlineCPTypes.length > 0);
 });
+const showBusinessHours = hasOfflineSelected;
 
-const prevOfflineCPTypes = ref<string[] | undefined>(undefined);
 let wasCleared = false;
 
 // 重置营业时间为默认值
@@ -546,25 +526,20 @@ function resetBusinessHours() {
   });
 }
 
-// 监听线下项目变化,更新表单项,并在清除后再次选择时重置营业时间
-watch(() => formProps.data?.offlineCPTypes, (newVal) => {
-  const newHasValue = !!(newVal && Array.isArray(newVal) && newVal.length > 0);
-  const oldHasValue = !!(prevOfflineCPTypes.value && Array.isArray(prevOfflineCPTypes.value) && prevOfflineCPTypes.value.length > 0);
-
-
-  if (oldHasValue && !newHasValue) {
-    wasCleared = true;
-  }
-
-
-  if (wasCleared && !oldHasValue && newHasValue) {
-    resetBusinessHours();
-    wasCleared = false;
-  }
-
-  prevOfflineCPTypes.value = newVal ? [...newVal] : undefined;
-  updateFormItems();
-}, { deep: true });
+// 仅在“是否选中线下服务”发生变化时处理:
+// - 避免每次勾选/取消一个选项都触发表单 items 重建,导致下拉框自动收起
+// - 保持原逻辑:清空后再次选择时重置营业时间;并联动显示/隐藏营业时间字段
+watch(
+  hasOfflineSelected,
+  (newHasValue, oldHasValue) => {
+    if (oldHasValue && !newHasValue) wasCleared = true;
+    if (wasCleared && !oldHasValue && newHasValue) {
+      resetBusinessHours();
+      wasCleared = false;
+    }
+  },
+  { immediate: true }
+);
 
 const formEmits: VxeFormListeners<FormModel> = {
   submit({ data }) {
@@ -675,15 +650,11 @@ onBeforeMount(async () => {
   if (props.data?.id) load(props.data);
   await getOnlineCPList();
   await getOfflineCPList();
-  // 初始化线下服务
-  prevOfflineCPTypes.value = formProps.data?.offlineCPTypes ? [...formProps.data.offlineCPTypes] : undefined;
-  // 初始化表单
-  updateFormItems();
 });
 </script>
 
 <template>
-  <div class="form-container">
+  <div class="form-container" :class="{ 'business-hours-hidden': !showBusinessHours }">
     <vxe-form ref="formRef" v-bind="formProps" v-on="formEmits" :loading="submitting">
       <template #initiate>
         <a-tree-select style="width: 100%" :show-checked-strategy="SHOW_ALL" tree-check-strictly :tree-data="branch"
@@ -728,6 +699,12 @@ onBeforeMount(async () => {
   padding: 20px;
 }
 
+.form-container.business-hours-hidden {
+  :deep(.business-hours-item) {
+    display: none !important;
+  }
+}
+
 .business-hours-container {
   width: 100%;
 }

+ 77 - 63
src/model/order.model.ts

@@ -7,43 +7,43 @@
 // }
 
 export interface OrderModel {
-id: number; //	线下服务ID
-patientConditioningProgramId: number; //患者调理方案id
-sequence: number; //次序
-liaison:string;//联系人
-phone: string; //联系电话
-provinceName: string; //省名称
-cityName: string; //市名称
-areaName: string; //区名称
-detailAddress: string; //详细地址
-orderNo: string; //订单编号
-institutionId: number; //订单所属机构ID
-institutionName: string; //订单所属机构名称
-conditioningProgramName: string; //项目名称
-conditioningProgramType: string; //项目类型
-applyTime:string;//用户操作时间
-offlineDuration:string;//	服务时长
-arrangeDate:string;//预约日期
-arrangeTime:string;//预约时间段-开始时间
-arrangeEndTime:string;//预约时间段-结束时间
-conditioningProgramSupplierId:number;//本次供应商ID
-conditioningProgramSupplierName:string;//本次供应商名称
-preConditioningProgramSupplierId:number;//上次供应商ID
-preConditioningProgramSupplierName:string;//上次供应商名称
-pieBy:string;//派单人
-pieTime:string;//派单时间
-operateBy:string;//操作者
-operateTime:string;//操作时间
-startTime:string;//操作开始时间
-endTime:string;//操作结束时间
-feedback:string;//治疗备注
-photo:string;//图片
-type:number;//订单类型 1-未派单 2-已派单但未核销 3-已核销
-// 实体商品
-receiptStatus:string;//收货状态 0-待发货 1-已发货 2-已收货
-types:string[];//订单类型 1-未派单 2-已派单但未核销 3-已核销	
-pieTimeStart:string;//派单时间开始
-pieTimeEnd:string;//派单时间结束
+  id: number; //	线下服务ID
+  patientConditioningProgramId: number; //患者调理方案id
+  sequence: number; //次序
+  liaison: string;//联系人
+  phone: string; //联系电话
+  provinceName: string; //省名称
+  cityName: string; //市名称
+  areaName: string; //区名称
+  detailAddress: string; //详细地址
+  orderNo: string; //订单编号
+  institutionId: number; //订单所属机构ID
+  institutionName: string; //订单所属机构名称
+  conditioningProgramName: string; //项目名称
+  conditioningProgramType: string; //项目类型
+  applyTime: string;//用户操作时间
+  offlineDuration: string;//	服务时长
+  arrangeDate: string;//预约日期
+  arrangeTime: string;//预约时间段-开始时间
+  arrangeEndTime: string;//预约时间段-结束时间
+  conditioningProgramSupplierId: number;//本次供应商ID
+  conditioningProgramSupplierName: string;//本次供应商名称
+  preConditioningProgramSupplierId: number;//上次供应商ID
+  preConditioningProgramSupplierName: string;//上次供应商名称
+  pieBy: string;//派单人
+  pieTime: string;//派单时间
+  operateBy: string;//操作者
+  operateTime: string;//操作时间
+  startTime: string;//操作开始时间
+  endTime: string;//操作结束时间
+  feedback: string;//治疗备注
+  photo: string;//图片
+  type: number;//订单类型 1-未派单 2-已派单但未核销 3-已核销
+  // 实体商品
+  receiptStatus: string;//收货状态 0-待发货 1-已发货 2-已收货
+  types: string[];//订单类型 1-未派单 2-已派单但未核销 3-已核销	
+  pieTimeStart: string;//派单时间开始
+  pieTimeEnd: string;//派单时间结束
 }
 export type OrderQuery = Partial<OrderModel>;
 //派单机构列表
@@ -90,23 +90,23 @@ export type OrderLiaisonListQuery = Partial<OrderLiaisonListModel>;
 
 
 
-export interface ShipmentModel{
+export interface ShipmentModel {
   id: number; //	患者调理方案ID
   orderNo: string; //订单ID
-  conditioningProgramSupplierId:number;//供应商ID
-  conditioningProgramSupplierName:string;//供应商名称
-  payTimeStart:string;//付款时间——起始时间	
-  payTimeEnd:string;//付款时间——截止时间
+  conditioningProgramSupplierId: number;//供应商ID
+  conditioningProgramSupplierName: string;//供应商名称
+  payTimeStart: string;//付款时间——起始时间	
+  payTimeEnd: string;//付款时间——截止时间
   payTime: string; //订单编号
   conditioningProgramName: string; //调理方案名称
   pricingUnit: string; //计价单位
   unitPrice: number; //单价
-  totalMeasure:number;//总用量
-  totalPrice:number;//总价格
-  receiptStatus:string;//发货状态 0-待发货 1-已发货 2-已收货
-  receiptType:string;//	发货形式 0-配送 1-线下取货
-  expressType:string;//快递类型 0-邮政速递 1-顺丰速运 2-京东快递 3-中通快递 4-圆通速递 5-申通快递 6-韵达快递 7-极兔速递
-  expressNo:string;//快递单号
+  totalMeasure: number;//总用量
+  totalPrice: number;//总价格
+  receiptStatus: string;//发货状态 0-待发货 1-已发货 2-已收货
+  receiptType: string;//	发货形式 0-配送 1-线下取货
+  expressType: string;//快递类型 0-邮政速递 1-顺丰速运 2-京东快递 3-中通快递 4-圆通速递 5-申通快递 6-韵达快递 7-极兔速递
+  expressNo: string;//快递单号
 
 }
 export type ShipmentQuery = Partial<ShipmentModel>;
@@ -128,23 +128,37 @@ export interface RevenueSharingDetailModel {
   id: number; //	主键ID
   orderNo: string; //订单ID
   payTime: string; //付款时间
-  profitSharingTimeStart:string;//分账时间开始
-  profitSharingTimeEnd:string;//分账时间结束
-  finishTime:string;//收货/核销/完成时间
-  profitSharingTime:string;//分账时间
+  profitSharingTimeStart: string;//分账时间开始
+  profitSharingTimeEnd: string;//分账时间结束
+  finishTime: string;//收货/核销/完成时间
+  profitSharingTime: string;//分账时间
   conditioningProgramName: string; //调理方案名称
-  conditioningProgramType:string;//调理方案类型
-  sellType:string;//	商品类型 1-实体商品 2-线下服务 3-线上权益
+  conditioningProgramType: string;//调理方案类型
+  sellType: string;//	商品类型 1-实体商品 2-线下服务 3-线上权益
   pricingUnit: string; //计价单位
   unitPrice: number; //单价
-  totalMeasure:number;//总用量
-  totalPrice:number;//总价格
-  conditioningProgramSupplierName:string;//供应商名称
-  profitSharing:string;//分账比例
-  profitSharingAmount:number;//预计分账金额分账金额
-  realAmount:string;//实际到账金额
-  profitSharingStatus:string;//分账状态 1-未分账 2-已分账 3-分账异常
-  
+  totalMeasure: number;//总用量
+  totalPrice: number;//总价格
+  conditioningProgramSupplierName: string;//供应商名称
+  profitSharing: string;//分账比例
+  profitSharingAmount: number;//预计分账金额分账金额
+  realAmount: string;//实际到账金额
+  profitSharingStatus: string;//分账状态 1-未分账 2-已分账 3-分账异常
+
 
 }
-export type RevenueSharingDetailQuery = Partial<RevenueSharingDetailModel>;
+export type RevenueSharingDetailQuery = Partial<RevenueSharingDetailModel>;
+
+
+export interface EvaluateDetailModel {
+  patientConditioningRecordId: number; //	患者调理记录ID
+  patientConditioningProgramId: string; //患者调理方案ID
+  lineId: string; //调理任务ID
+  complianceScore: string;//相符度评分
+  qualityScore: string;//质量评分
+  attitudeScore: string;//	态度评分
+  environmentScore: string;//环境评分
+  depict: string;//	评价描述
+  imageVideos: string[];//图片视频
+  createTime: string;//创建时间
+}

+ 1 - 0
src/order/DispatchOrderPanel.vue

@@ -171,6 +171,7 @@ const gridOptions = reactive<VxeGridProps<Institution>>({
     { field: 'detailAddress', title: '地址' },
     { field: 'phone', title: '联系电话' },
     { field: 'todayOrderQuantity', title: '当日订单数' },
+    { field: 'evaluateScore', title: '机构评分' },
   ],
   data: [],
   pagerConfig: {

+ 4 - 0
src/pages/index/care/conditioningRecord.vue

@@ -75,6 +75,10 @@ const gridOptions = reactive<VxeGridProps<ConditioningRecordListModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 5 - 0
src/pages/index/care/configured.vue

@@ -139,6 +139,10 @@ const gridOptions = reactive<VxeGridProps<ConditioningSchemeModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -153,6 +157,7 @@ const gridOptions = reactive<VxeGridProps<ConditioningSchemeModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'orgName', title: '组织名称' },
     { field: 'insName', title: '机构名称' },
     // {

+ 4 - 0
src/pages/index/care/institutionService.vue

@@ -94,6 +94,10 @@ const gridOptions = reactive<VxeGridProps<SystemCwModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 5 - 0
src/pages/index/care/supplier.vue

@@ -119,6 +119,10 @@ const gridOptions = reactive<VxeGridProps<SupplierModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -133,6 +137,7 @@ const gridOptions = reactive<VxeGridProps<SupplierModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'name', title: '供应商' },
     { field: 'detailAddress', title: '地址' },
     { field: 'kahuna', title: '负责人' },

+ 4 - 0
src/pages/index/care/systemService.vue

@@ -80,6 +80,10 @@ const gridOptions = reactive<VxeGridProps<SystemCwModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 1 - 0
src/pages/index/equipment/configured.vue

@@ -190,6 +190,7 @@ const gridOptions = reactive<VxeGridProps<DeviceManageModel>>({
   },
   columns: [
     { type: 'checkbox', width: 60, fixed: 'left', title: '', align: 'center' },
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'orgName', title: '组织名称' },
     { field: 'warrant', title: '设备ID' },
     {

+ 5 - 0
src/pages/index/equipment/registe.vue

@@ -186,6 +186,10 @@ const gridOptions = reactive<VxeGridProps<EquirementModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -201,6 +205,7 @@ const gridOptions = reactive<VxeGridProps<EquirementModel>>({
   },
   columns: [
     { type: 'checkbox', width: 100, fixed: 'left', title: '', align: 'center' },
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'deviceType', title: '设备名称' },
     { field: 'orgName', title: '组织名称' },
     { field: 'institutionName', title: '机构名称' },

+ 5 - 0
src/pages/index/equipment/reportManagement.vue

@@ -156,6 +156,10 @@ const gridOptions = reactive<any>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -170,6 +174,7 @@ const gridOptions = reactive<any>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'orgName', title: '组织名称' },
     { field: 'deviceType', title: '设备名称' },
     { field: 'deviceCode', title: '设备ID' },

+ 5 - 0
src/pages/index/follow/assessment.vue

@@ -100,6 +100,10 @@ const gridOptions = reactive<VxeGridProps<EvaluationModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -111,6 +115,7 @@ const gridOptions = reactive<VxeGridProps<EvaluationModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'followupPlanName', title: '随访计划' },
     { field: 'patientName', title: '姓名' },
     { field: 'sex', title: '性别', slots: { default: 'sex' } },

+ 4 - 0
src/pages/index/follow/plan.vue

@@ -129,6 +129,10 @@ const gridOptions = reactive<VxeGridProps<PlanModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 5 - 0
src/pages/index/follow/task.vue

@@ -114,6 +114,10 @@ const gridOptions = reactive<VxeGridProps<TaskModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -126,6 +130,7 @@ const gridOptions = reactive<VxeGridProps<TaskModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'followupTaskName', title: '随访计划' },
     { field: 'patientName', title: '姓名' },
     { field: 'sex', title: '性别', slots: { default: 'sex' } },

+ 4 - 0
src/pages/index/healthy/education.vue

@@ -80,6 +80,10 @@ const gridOptions = reactive<VxeGridProps<EducationModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/order/management.vue

@@ -130,6 +130,10 @@ const gridOptions = reactive<VxeGridProps<ConditioningRecordListModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/order/revenueSharing.vue

@@ -122,6 +122,10 @@ const gridOptions = reactive<VxeGridProps<RevenueSharingDetailModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/order/shipment.vue

@@ -180,6 +180,10 @@ const gridOptions = reactive<VxeGridProps<ShipmentModel>>({
 
     return { rowspan: 1, colspan: 1 };
   },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/patient/history.vue

@@ -169,6 +169,10 @@ const gridOptions = reactive<VxeGridProps<PatientReportModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 5 - 0
src/pages/index/system/institution.vue

@@ -55,6 +55,10 @@ const gridOptions = reactive<VxeGridProps<InstitutionModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -166,6 +170,7 @@ function editInstitution(model?: InstitutionModel, index?: number) {
 }
 
 function QRCode(model: InstitutionModel) {
+  console.log(model,111);
   const { name } = model;
   VxeUI.modal.open({
     title: `${name} 专属小程序码`,

+ 4 - 0
src/pages/index/system/organization.vue

@@ -55,6 +55,10 @@ const gridOptions = reactive<VxeGridProps<OrganizationModel>>({
   autoResize: false,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/system/role.vue

@@ -59,6 +59,10 @@ const gridOptions = reactive<VxeGridProps<RoleModel>>({
   showOverflow: true,
   height: 'auto', autoResize: false, syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/system/tag.vue

@@ -92,6 +92,10 @@ const gridOptions = reactive<VxeGridProps<TagModel>>({
   showOverflow: true,
   height: 'auto', autoResize: false, syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/pages/index/system/user.vue

@@ -66,6 +66,10 @@ const gridOptions = reactive<VxeGridProps<UserModel>>({
   showOverflow: true,
   height: 'auto', autoResize: false, syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 16 - 1
src/request/api/order.api.ts

@@ -1,5 +1,8 @@
 import type { List, Tree } from '@/model';
-import type { OrderQuery, OrderModel, OrderLiaisonListModel, OrderLiaisonListQuery, ShipmentModel, ShipmentQuery, PieOrderCountModel,RevenueSharingDetailModel,RevenueSharingDetailQuery } from '@/model/order.model';
+import type {
+  OrderQuery,
+  OrderModel, OrderLiaisonListModel, ShipmentModel, ShipmentQuery, PieOrderCountModel, RevenueSharingDetailModel, RevenueSharingDetailQuery, EvaluateDetailModel
+} from '@/model/order.model';
 import request from '@/request/alova';
 
 // 线下服务  今日指派订单分页列表
@@ -163,4 +166,16 @@ export function getRevenueSharingDetailListMethod(page: number, size: number, qu
       hitSource: /revenueSharing$/, // 匹配失效源
     }
   );
+}
+
+
+// 获取订单商品-线下核销记录的患者个人评价   type 	1-商品 2-线下核销记录
+export function getEvaluateDetailMethod(type: string, id: string) {
+  return request.Post<EvaluateDetailModel>(
+    `/fdhb-pc/patientCrManage/getEvaluate/${type}/${id}`,
+    {},
+    {
+      hitSource: /order$/, // 匹配失效源
+    }
+  );
 } 

+ 4 - 0
src/satisfaction/SendRecord.vue

@@ -87,6 +87,10 @@ const gridOptions = reactive<VxeGridProps<SatisfactionSendRecordModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/satisfaction/SurveyList.vue

@@ -89,6 +89,10 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/service/NotifyManageList.vue

@@ -86,6 +86,10 @@ const gridOptions = reactive<VxeGridProps<NotifyModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 4 - 0
src/service/NotifyManageRecord.vue

@@ -87,6 +87,10 @@ const gridOptions = reactive<VxeGridProps<NotifyRecordModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,

+ 123 - 0
src/service/ReviewMediaPreview.vue

@@ -0,0 +1,123 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+// @ts-expect-error swiper 模块导出与类型声明解析
+import { Navigation, Pagination } from 'swiper';
+import { Swiper, SwiperSlide } from 'swiper/vue';
+import 'swiper/css';
+import 'swiper/css/navigation';
+import 'swiper/css/pagination';
+
+export type MediaItem = { type: 'image' | 'video'; url: string };
+
+defineProps<{
+  mediaList: MediaItem[];
+  initialIndex: number;
+}>();
+
+const carouselRef = ref<HTMLElement | null>(null);
+const swiperModules = [Navigation, Pagination];
+
+function onSlideChange() {
+  carouselRef.value?.querySelectorAll('video').forEach((v) => v.pause());
+}
+</script>
+
+<template>
+  <div ref="carouselRef" class="review-preview-carousel">
+    <swiper
+      v-if="mediaList?.length"
+      :modules="swiperModules"
+      :initial-slide="initialIndex"
+      :slides-per-view="1"
+      :space-between="0"
+      navigation
+      pagination
+      class="review-preview-swiper"
+      @slide-change="onSlideChange"
+    >
+      <swiper-slide v-for="(media, i) in mediaList" :key="i" class="review-preview-slide">
+        <div v-if="media.type === 'image'" class="review-preview-inner">
+          <img :src="media.url" alt="" class="review-preview-img" />
+        </div>
+        <div v-else class="review-preview-inner review-preview-video-wrap">
+          <video :src="media.url" controls class="review-preview-video" />
+        </div>
+      </swiper-slide>
+    </swiper>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.review-preview-carousel {
+  width: 100%;
+  height: 70vh;
+  min-height: 300px;
+
+  .review-preview-swiper {
+    width: 100%;
+    height: 100%;
+    --swiper-navigation-size: 36px;
+    --swiper-theme-color: #1890ff;
+  }
+
+  .review-preview-slide {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background: #000;
+    box-sizing: border-box;
+  }
+
+  .review-preview-inner {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    box-sizing: border-box;
+  }
+
+  .review-preview-img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+    display: block;
+  }
+
+  .review-preview-video-wrap {
+    width: 100%;
+    height: 100%;
+  }
+
+  .review-preview-video {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+    display: block;
+  }
+
+  :deep(.swiper-wrapper) {
+    height: 100%;
+  }
+
+  :deep(.swiper-slide) {
+    height: 100%;
+  }
+
+  :deep(.swiper-button-prev),
+  :deep(.swiper-button-next) {
+    color: #fff;
+  }
+
+  :deep(.swiper-pagination-bullet) {
+    background: #fff;
+    opacity: 0.5;
+  }
+
+  :deep(.swiper-pagination-bullet-active) {
+    opacity: 1;
+  }
+}
+</style>

+ 5 - 0
src/service/ServiceItemsConfirm.vue

@@ -66,6 +66,10 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -81,6 +85,7 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'name', title: '项目名称' },
     { field: 'conditioningProgramType', title: '方案类型' },
     { field: 'cpFixedPricingRule.unitPrice', title: '单价(元)', slots: { default: 'unitPriceCell' } },

+ 5 - 0
src/service/ServiceItemsList.vue

@@ -118,6 +118,10 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -133,6 +137,7 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
     storage: true,
   },
   columns: [
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'name', title: '项目名称' },
     { field: 'conditioningProgramType', title: '方案类型' },
     { field: 'isForWrapCell', title: '项目应用', slots: { default: 'isForWrapCell' } },

+ 5 - 0
src/service/ServiceItemsSystem.vue

@@ -105,6 +105,10 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   autoResize: true,
   syncResize: true,
   scrollY: { enabled: true, gt: 0 },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   toolbarConfig: {
     custom: true,
     zoom: true,
@@ -121,6 +125,7 @@ const gridOptions = reactive<VxeGridProps<SystemItemModel>>({
   },
   columns: [
     { type: 'checkbox', width: 100, fixed: 'left', title: '批量' },
+    { type: 'seq', title: '序号', width: 80 },
     { field: 'name', title: '项目名称' },
     { field: 'conditioningProgramType', title: '方案类型' },
     { field: 'cpFixedPricingRule.unitPrice', title: '单价(元)', slots: { default: 'unitPriceCell' }, width: 150 },

+ 319 - 6
src/service/SingleItemDetail.vue

@@ -1,10 +1,35 @@
 <script setup lang="ts">
 import type { SystemCwModel } from '@/model/care.model';
-import { computed } from 'vue';
+import { PlayCircleOutlined } from '@ant-design/icons-vue';
+import { computed, h, onMounted, ref } from 'vue';
 import { notification } from 'ant-design-vue';
+import VxeUI from 'vxe-table';
+import ReviewMediaPreview from '@/service/ReviewMediaPreview.vue';
+import type { MediaItem } from '@/service/ReviewMediaPreview.vue';
+import seeEvaluate from '@/service/seeEvaluate.vue';
+import { getEvaluateDetailMethod } from '@/request/api/order.api';
+
+
 const props = defineProps<{
   data: SystemCwModel['items'][number];
 }>();
+// 服务记录--查看评价
+function openSeeEvaluate(row: any) {
+    VxeUI.modal.open({
+      title: '用户评价',
+      width: 900,
+      height: 600,
+      escClosable: true,
+      destroyOnClose: true,
+      slots: {
+        default() {
+          return h(seeEvaluate, {
+            data: row,
+          });
+        },
+      },
+    });
+}
 // 复制物流信息
 function handleCopyTracking() {
   const trackingNumber = mockLogisticsData.value.trackingNumber;
@@ -42,8 +67,6 @@ const expressTypeText: Record<string, string> = {
 };
 // 包裹数据
 const mockPackageItems = computed(() => {
-  console.log('props.data===', props.data);
-
   return (props.data as any).sameExpress ?? []
 });
 // 商品状态映射 - 收货状态(实体商品使用)
@@ -64,6 +87,89 @@ const sellTypeText: Record<string, string> = {
   '2': '线下服务',
   '3': '线上权益',
 };
+
+function toScore(value: unknown) {
+  const n = Number(value);
+  return Number.isFinite(n) ? n : 0;
+}
+
+function normalizeMediaList(input: unknown): MediaItem[] {
+  if (!Array.isArray(input)) return [];
+  return input
+    .map((it) => {
+      if (typeof it === 'string') {
+        const url = it;
+        const type: MediaItem['type'] = /\.(mp4|webm|ogg)(\?|#|$)/i.test(url) ? 'video' : 'image';
+        return { type, url } as MediaItem;
+      }
+      if (it && typeof it === 'object' && 'url' in it) {
+        const anyIt = it as any;
+        const url = String(anyIt.url ?? '');
+        if (!url) return null;
+        const type: MediaItem['type'] =
+          anyIt.type === 'video' || anyIt.type === 'image'
+            ? anyIt.type
+            : /\.(mp4|webm|ogg)(\?|#|$)/i.test(url)
+              ? 'video'
+              : 'image';
+        return { type, url } as MediaItem;
+      }
+      return null;
+    })
+    .filter((x): x is MediaItem => Boolean(x));
+}
+
+// 用户评价(与 seeEvaluate.vue 逻辑一致)
+const scroeType = ref([
+  { name: '描述相符', score: 0 },
+]);
+
+const evaluateDetail = ref({
+  depict: '',
+  mediaList: [] as MediaItem[],
+});
+
+const hasEvaluate = computed(() => {
+  const depict = (evaluateDetail.value.depict ?? '').trim();
+  const hasMedia = (evaluateDetail.value.mediaList?.length ?? 0) > 0;
+  const hasScore = scroeType.value.some((i) => toScore(i.score) > 0);
+  return Boolean(depict) || hasMedia || hasScore;
+});
+
+async function getEvaluateDetail() {
+  const id = (props.data as any)?.id;
+  if (!id) return;
+  const res = await getEvaluateDetailMethod('1', id);
+  if (!res) return;
+  if (scroeType.value[0]) scroeType.value[0].score = toScore(res.complianceScore);
+  evaluateDetail.value.depict = res.depict ?? '';
+  evaluateDetail.value.mediaList = normalizeMediaList(res.imageVideos);
+}
+
+const REVIEW_PREVIEW_MODAL_ID = 'review-media-preview-modal';
+//预览图片/视频
+function openPreview(list: MediaItem[], index: number) {
+  if (!list?.length) return;
+  VxeUI.modal.open({
+    id: REVIEW_PREVIEW_MODAL_ID,
+    title: '预览',
+    width: 720,
+    escClosable: true,
+    destroyOnClose: true,
+    slots: {
+      default() {
+        return h(ReviewMediaPreview, {
+          mediaList: list,
+          initialIndex: index,
+        });
+      },
+    },
+  });
+}
+
+onMounted(() => {
+  getEvaluateDetail();
+});
 </script>
 
 <template>
@@ -164,7 +270,7 @@ const sellTypeText: Record<string, string> = {
       <!-- receiptType	收货方式 0-快递 1-线下取货 -->
       <!-- <h3 class="info-title">{{ data.receiptType === '0' ? '物流信息' : data.receiptType === '1' ? '线下取货' : '' }}</h3> -->
       <h3 class="info-title">物流信息</h3>
-    
+
       <div class="info-content-wrapper" v-if="data.receiptType">
         <!-- 物流信息 -->
         <div class="logistics-content" v-if="data.receiptType === '0'">
@@ -196,7 +302,7 @@ const sellTypeText: Record<string, string> = {
             包裹内商品
           </h3>
           <div class="package-items">
-          
+
             <div v-for="(item, index) in mockPackageItems" :key="index" class="package-item">
               <div class="package-item-image" v-if="item.conditioningProgramPhoto">
                 <a-image v-if="item.conditioningProgramPhoto" :width="60" :height="60" style="border-radius: 4px;"
@@ -233,7 +339,7 @@ const sellTypeText: Record<string, string> = {
         <vxe-column type="seq" title="序号" width="60" align="center" />
         <vxe-column field="arrangeDate" title="服务日期" align="center" />
         <vxe-column field="arrangePeriod" title="服务时间段" align="center" />
-        <vxe-column field="operateTime" title="服务状态" align="center">
+        <vxe-column field="verifyStatus" title="服务状态" align="center">
           <template #default="{ row }">
             {{ row.operateTime ? '已核销' : '已预约' }}
           </template>
@@ -257,8 +363,55 @@ const sellTypeText: Record<string, string> = {
         </vxe-column>
         <vxe-column field="operateBy" title="操作人" align="center" />
         <vxe-column field="feedback" title="治疗备注" align="center" />
+        <vxe-column field="acuPointNames" title="用户评价" align="center">
+          <template #default="{ row }">
+            <vxe-button @click="openSeeEvaluate(row)" content="查看" status="primary" />
+          </template>
+        </vxe-column>
       </vxe-table>
     </div>
+    <!-- 用户评价 -->
+    <div class="info-section">
+      <div class="info-title-row">
+        <h3 class="info-title">用户评价</h3>
+      </div>
+      <div class="info-content-wrapper">
+        <template v-if="hasEvaluate">
+          <div class="review-item">
+            <div class="review-top-row">
+              <div class="review-rating-box">
+                <div class="review-rating-row" v-for="scroe in scroeType" :key="scroe.name">
+                  <span class="review-criterion">{{ scroe.name }}</span>
+                  <a-rate :value="scroe.score" disabled allow-half class="review-stars" />
+                  <span class="review-score">{{ scroe.score }}分</span>
+                </div>
+              </div>
+              <div class="review-content-block">
+                <div class="review-content-line">
+                  <span class="review-content-label">评价内容:</span>
+                  <span class="review-content-text">{{ evaluateDetail.depict }}</span>
+                </div>
+                <div v-if="evaluateDetail.mediaList?.length" class="review-media-row">
+                  <div v-for="(media, i) in evaluateDetail.mediaList" :key="i" class="review-media-thumb"
+                    :class="{ 'is-video': media.type === 'video' }" role="button" tabindex="0"
+                    @click.stop.prevent="openPreview(evaluateDetail.mediaList, i)"
+                    @keydown.enter.space.prevent="openPreview(evaluateDetail.mediaList, i)">
+                    <img v-if="media.type === 'image'" :src="media.url" class="review-thumb-img" alt="" />
+                    <div v-else class="review-thumb-video">
+                      <video :src="media.url" muted preload="metadata" />
+                      <span class="review-thumb-play">
+                        <PlayCircleOutlined />
+                      </span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </template>
+        <div v-else class="empty-text">无</div>
+      </div>
+    </div>
   </div>
 </template>
 
@@ -299,6 +452,159 @@ const sellTypeText: Record<string, string> = {
     color: #333;
   }
 
+  .info-title-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 12px;
+
+    .info-title {
+      margin-bottom: 0;
+    }
+  }
+
+  .review-item {
+    padding: 12px 0;
+    border-bottom: 1px solid #f0f0f0;
+
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+
+  .review-top-row {
+    display: flex;
+    align-items: flex-start;
+    gap: 24px;
+    margin-bottom: 12px;
+  }
+
+  .review-rating-box {
+    flex-shrink: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 12px;
+    min-width: 240px;
+  }
+
+  .review-rating-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    flex-shrink: 0;
+  }
+
+  .review-criterion {
+    font-size: 14px;
+    color: #333;
+    width: 72px;
+    text-align: left;
+    white-space: nowrap;
+  }
+
+  .review-stars {
+    font-size: 16px;
+
+    :deep(.ant-rate-star-full .ant-rate-star-second) {
+      color: #fadb14;
+    }
+
+    :deep(.ant-rate-star-full .ant-rate-star-first) {
+      color: #fadb14;
+    }
+  }
+
+  .review-score {
+    font-size: 13px;
+    color: #999;
+  }
+
+  .review-content-block {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .review-content-line {
+    display: flex;
+    align-items: flex-start;
+    gap: 6px;
+  }
+
+  .review-content-label {
+    font-size: 14px;
+    color: #333;
+    margin-right: 4px;
+    flex-shrink: 0;
+  }
+
+  .review-content-text {
+    font-size: 14px;
+    color: #333;
+    line-height: 22px;
+    margin: 0;
+    white-space: pre-wrap;
+    max-width: 560px;
+    word-break: break-word;
+  }
+
+  .review-media-row {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+    margin-top: 12px;
+    margin-left: 70px;
+  }
+
+  .review-media-thumb {
+    width: 72px;
+    height: 72px;
+    border-radius: 6px;
+    border: 1px solid #e8e8e8;
+    overflow: hidden;
+    flex-shrink: 0;
+    cursor: pointer;
+    background: #f5f5f5;
+    transition: border-color 0.2s, box-shadow 0.2s;
+
+    &:hover {
+      border-color: #1890ff;
+      box-shadow: 0 0 0 1px #1890ff;
+    }
+
+    .review-thumb-img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      display: block;
+    }
+
+    .review-thumb-video {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      background: #2a2a2a;
+
+      video {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+        display: block;
+      }
+
+      .review-thumb-play {
+        position: absolute;
+        inset: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: rgba(255, 255, 255, 0.9);
+        font-size: 28px;
+        pointer-events: none;
+      }
+    }
+  }
+
   .info-content-wrapper .info-title {
     margin-top: 20px;
     margin-bottom: 12px;
@@ -333,6 +639,13 @@ const sellTypeText: Record<string, string> = {
     }
   }
 
+  .empty-text {
+    color: #999;
+    font-size: 14px;
+    line-height: 22px;
+    padding: 8px 0;
+  }
+
   .product-info {
     display: flex;
     align-items: flex-start;

+ 340 - 0
src/service/seeEvaluate.vue

@@ -0,0 +1,340 @@
+<script setup lang="ts">
+import { PlayCircleOutlined } from '@ant-design/icons-vue';
+import { computed, h, onMounted, ref } from 'vue';
+import VxeUI from 'vxe-table';
+import ReviewMediaPreview from '@/service/ReviewMediaPreview.vue';
+import type { MediaItem } from '@/service/ReviewMediaPreview.vue';
+import { getEvaluateDetailMethod } from '@/request/api/order.api';
+const props = defineProps<{
+    data: any;
+}>();
+
+function toScore(value: unknown) {
+    const n = Number(value);
+    return Number.isFinite(n) ? n : 0;
+}
+
+function normalizeMediaList(input: unknown): MediaItem[] {
+    if (!Array.isArray(input)) return [];
+    return input
+        .map((it) => {
+            if (typeof it === 'string') {
+                const url = it;
+                const type: MediaItem['type'] = /\.(mp4|webm|ogg)(\?|#|$)/i.test(url) ? 'video' : 'image';
+                return { type, url } as MediaItem;
+            }
+            if (it && typeof it === 'object' && 'url' in it) {
+                const anyIt = it as any;
+                const url = String(anyIt.url ?? '');
+                if (!url) return null;
+                const type: MediaItem['type'] =
+                    anyIt.type === 'video' || anyIt.type === 'image'
+                        ? anyIt.type
+                        : /\.(mp4|webm|ogg)(\?|#|$)/i.test(url)
+                            ? 'video'
+                            : 'image';
+                return { type, url } as MediaItem;
+            }
+            return null;
+        })
+        .filter((x): x is MediaItem => Boolean(x));
+}
+const scroeType = ref([{
+    name: '服务质量',
+    score: 0,
+}, {
+    name: '服务态度',
+    score: 0,
+}, {
+    name: '环境',
+    score: 0,
+}]);
+// 用户评价假数据(淘宝式:每条评价有多张图/多个视频,先展示缩略图排列,点击可左右滑动预览)
+const evaluateDetail = ref(
+    {
+        depict: '',
+        // 图片/视频列表
+        mediaList: [] as MediaItem[],
+    });
+
+const hasEvaluate = computed(() => {
+    const depict = String(evaluateDetail.value.depict ?? '').trim();
+    const hasMedia = (evaluateDetail.value.mediaList?.length ?? 0) > 0;
+    const hasScore = scroeType.value.some((i) => toScore(i.score) > 0);
+    return Boolean(depict) || hasMedia || hasScore;
+});
+
+const REVIEW_PREVIEW_MODAL_ID = 'review-media-preview-modal';
+async function getEvaluateDetail() {
+    console.log(props.data, "传过来的参数");
+    if (props.data && props.data.id) {
+        const res = await getEvaluateDetailMethod('2', props.data.id);
+        console.log(res, "获取评价");
+        if (res) {
+            if (scroeType.value[0]) scroeType.value[0].score = toScore(res.qualityScore); //服务质量
+            if (scroeType.value[1]) scroeType.value[1].score = toScore(res.attitudeScore); //服务态度
+            if (scroeType.value[2]) scroeType.value[2].score = toScore(res.environmentScore); //环境
+            evaluateDetail.value.depict = res.depict;
+            evaluateDetail.value.mediaList = normalizeMediaList(res.imageVideos);
+        }
+    }
+    console.log(evaluateDetail.value, "evaluateDetail");
+}
+
+onMounted(() => {
+    // 获取用户评价
+    getEvaluateDetail();
+});
+
+
+
+// 预览图片/视频
+function openPreview(list: MediaItem[], index: number) {
+    if (!list?.length) return;
+    VxeUI.modal.open({
+        id: REVIEW_PREVIEW_MODAL_ID,
+        title: '预览',
+        width: 720,
+        escClosable: true,
+        destroyOnClose: true,
+        slots: {
+            default() {
+                return h(ReviewMediaPreview, {
+                    mediaList: list,
+                    initialIndex: index,
+                });
+            },
+        },
+    });
+}
+</script>
+
+<template>
+    <div class="service-detail">
+        <!-- 用户评价 -->
+        <div class="info-section">
+            <div class="info-content-wrapper">
+                <template v-if="hasEvaluate">
+                    <div class="review-item">
+                        <div class="review-top-container">
+                            <div class="review-rating-box">
+                                <div class="review-rating-row" v-for="scroe in scroeType" :key="scroe.name">
+                                    <span class="review-criterion">{{ scroe.name }}</span>
+                                    <a-rate :value="scroe.score" disabled allow-half class="review-stars" />
+                                    <span class="review-score">{{ scroe.score }}分</span>
+                                </div>
+                            </div>
+                            <div class="review-content-block">
+                                <div class="review-content-line">
+                                    <span class="review-content-label">评价内容:</span>
+                                    <span class="review-content-text">{{ evaluateDetail.depict }}</span>
+                                </div>
+                                <div v-if="evaluateDetail.mediaList?.length" class="review-media-row">
+                                    <div v-for="(media, i) in evaluateDetail.mediaList" :key="i" class="review-media-thumb"
+                                        :class="{ 'is-video': media.type === 'video' }" role="button" tabindex="0"
+                                        @click.stop.prevent="openPreview(evaluateDetail.mediaList, i)"
+                                        @keydown.enter.space.prevent="openPreview(evaluateDetail.mediaList, i)">
+                                        <img v-if="media.type === 'image'" :src="media.url" class="review-thumb-img" alt="" />
+                                        <div v-else class="review-thumb-video">
+                                            <video :src="media.url" muted preload="metadata" />
+                                            <span class="review-thumb-play">
+                                                <PlayCircleOutlined />
+                                            </span>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </template>
+                <div v-else class="empty-text">无</div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style scoped lang="scss">
+.service-detail {
+    padding: 20px;
+    color: black;
+    background: #fff;
+
+    .info-section {
+        margin-top: 24px;
+        margin-bottom: 24px;
+        background: #fff;
+    }
+
+    .info-title {
+        font-size: 16px;
+        font-weight: 600;
+        margin-bottom: 12px;
+        color: #333;
+    }
+
+    .info-title-row {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-bottom: 12px;
+
+        .info-title {
+            margin-bottom: 0;
+        }
+    }
+
+    .info-content-wrapper {
+        border: 1px solid #e8e8e8;
+        border-radius: 4px;
+        padding: 10px 20px;
+    }
+
+    .empty-text {
+        color: #999;
+        font-size: 14px;
+        line-height: 22px;
+        padding: 8px 0;
+    }
+
+    .review-item {
+        padding: 12px 0;
+        border-bottom: 1px solid #f0f0f0;
+
+        &:last-child {
+            border-bottom: none;
+        }
+    }
+
+    .review-top-container {
+        display: flex;
+        align-items: flex-start;
+        gap: 24px;
+        margin-bottom: 12px;
+    }
+
+    .review-rating-box {
+        flex-shrink: 0;
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 12px;
+        min-width: 240px;
+    }
+
+    .review-rating-row {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        flex-shrink: 0;
+    }
+
+    .review-criterion {
+        font-size: 14px;
+        color: #333;
+        width: 72px;
+        text-align: left;
+        white-space: nowrap;
+    }
+
+    .review-stars {
+        font-size: 16px;
+
+        :deep(.ant-rate-star-full .ant-rate-star-second) {
+            color: #fadb14;
+        }
+
+        :deep(.ant-rate-star-full .ant-rate-star-first) {
+            color: #fadb14;
+        }
+    }
+
+    .review-score {
+        font-size: 13px;
+        color: #999;
+    }
+
+    .review-content-block {
+        flex: 1;
+        min-width: 0;
+    }
+
+    .review-content-line {
+        display: flex;
+        align-items: flex-start;
+        gap: 6px;
+    }
+
+    .review-content-label {
+        font-size: 14px;
+        color: #333;
+        margin-right: 4px;
+        flex-shrink: 0;
+    }
+
+    .review-content-text {
+        font-size: 14px;
+        color: #333;
+        line-height: 22px;
+        margin: 0;
+        white-space: pre-wrap;
+        word-break: break-word;
+    }
+
+    .review-media-row {
+        display: flex;
+        gap: 8px;
+        flex-wrap: wrap;
+        margin-top: 12px;
+        margin-left: 70px;
+    }
+
+    .review-media-thumb {
+        width: 72px;
+        height: 72px;
+        border-radius: 6px;
+        border: 1px solid #e8e8e8;
+        overflow: hidden;
+        flex-shrink: 0;
+        cursor: pointer;
+        background: #f5f5f5;
+        transition: border-color 0.2s, box-shadow 0.2s;
+
+        &:hover {
+            border-color: #1890ff;
+            box-shadow: 0 0 0 1px #1890ff;
+        }
+
+        .review-thumb-img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+            display: block;
+        }
+
+        .review-thumb-video {
+            position: relative;
+            width: 100%;
+            height: 100%;
+            background: #2a2a2a;
+
+            video {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                display: block;
+            }
+
+            .review-thumb-play {
+                position: absolute;
+                inset: 0;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                color: rgba(255, 255, 255, 0.9);
+                font-size: 28px;
+                pointer-events: none;
+            }
+        }
+    }
+}
+</style>

+ 4 - 0
src/widgets/PatientCareRecordsWidget.vue

@@ -23,6 +23,10 @@ const gridOptions = reactive<VxeGridProps<ConditioningRecordListModel>>({
   height: 'auto',
   headerAlign: 'center',
   align: 'center',
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   columnConfig: {
     resizable: true,
   },

+ 4 - 0
src/widgets/PatientDiagnosisRecordsWidget.vue

@@ -21,6 +21,10 @@ const gridOptions = reactive<VxeGridProps<DiagnosisReportVO>>({
   height: 'auto',
   headerAlign: 'center',
   align: 'center',
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   columnConfig: {
     resizable: true,
   },

+ 4 - 0
src/widgets/PatientFollowUpRecordsWidget.vue

@@ -23,6 +23,10 @@ const gridOptions = reactive<VxeGridProps<DiagnosisReportVO>>({
   height: 'auto',
   headerAlign: 'center',
   align: 'center',
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   columnConfig: {
     resizable: true,
   },

+ 4 - 0
src/widgets/PatientHealthRecordsWidget.vue

@@ -100,6 +100,10 @@ const gridOptions = reactive<VxeGridProps<Model>>({
   cellConfig: {
     height: 126,
   },
+  rowConfig: {
+    isHover: true,
+    isCurrent: true,
+  },
   columns: [
     { field: 'date', title: '日期', width: '150px', align: 'center', slots: { default: 'records' } },
     {