4
0

17 Commits fe344d90db ... 5df4dfbc7e

Autor SHA1 Mensagem Data
  张田田 5df4dfbc7e Merge branch 'feature/test' into develop há 1 mês atrás
  张田田 7e79798b87 Merge branch 'story-238' of ssh://121.43.162.141:10022/six.fe/health.admin into feature/test há 1 mês atrás
  张田田 f38eaf31f2 Merge branch 'develop' of ssh://121.43.162.141:10022/six.fe/health.admin into story-238 há 1 mês atrás
  张田田 842ecfeadf Merge branch 'story-237' of ssh://121.43.162.141:10022/six.fe/health.admin into feature/test há 1 mês atrás
  张田田 a8050e6a17 bug-620 há 1 mês atrás
  张田田 1970b8dae2 bug-605 há 1 mês atrás
  张田田 d6358c987d 取消预约功能 há 1 mês atrás
  张田田 01efdc6813 增加取消预约图片 há 1 mês atrás
  张田田 eea80ec407 测试 há 1 mês atrás
  张田田 8a20be2f89 bug-606 há 1 mês atrás
  张田田 034d586e6c bug-607 há 1 mês atrás
  张田田 d80eddd70e bug-610 há 1 mês atrás
  张田田 edee952619 Merge branch 'develop' of ssh://121.43.162.141:10022/six.fe/health.admin into story-237 há 2 meses atrás
  张田田 cbd97a0c30 Merge branch 'develop' of ssh://121.43.162.141:10022/six.fe/health.admin into story-237 há 2 meses atrás
  张田田 37af19f5f9 Merge branch 'develop' of ssh://121.43.162.141:10022/six.fe/health.admin into story-237 há 2 meses atrás
  张田田 8584004731 评价 há 2 meses atrás
  张田田 367d321840 评价 há 2 meses atrás
40 ficheiros alterados com 1221 adições e 200 exclusões
  1. BIN
      src/assets/images/cancel.png
  2. 36 59
      src/components/EditSupplier.vue
  3. 93 63
      src/model/order.model.ts
  4. 8 2
      src/order/DispatchOrderPanel.vue
  5. 4 0
      src/pages/index/care/conditioningRecord.vue
  6. 5 0
      src/pages/index/care/configured.vue
  7. 4 0
      src/pages/index/care/institutionService.vue
  8. 5 0
      src/pages/index/care/supplier.vue
  9. 4 0
      src/pages/index/care/systemService.vue
  10. 1 0
      src/pages/index/equipment/configured.vue
  11. 5 0
      src/pages/index/equipment/registe.vue
  12. 5 0
      src/pages/index/equipment/reportManagement.vue
  13. 5 0
      src/pages/index/follow/assessment.vue
  14. 4 0
      src/pages/index/follow/plan.vue
  15. 5 0
      src/pages/index/follow/task.vue
  16. 4 0
      src/pages/index/healthy/education.vue
  17. 4 0
      src/pages/index/order/management.vue
  18. 4 0
      src/pages/index/order/revenueSharing.vue
  19. 4 0
      src/pages/index/order/shipment.vue
  20. 4 0
      src/pages/index/patient/history.vue
  21. 53 29
      src/pages/index/system/institution.vue
  22. 52 29
      src/pages/index/system/organization.vue
  23. 4 0
      src/pages/index/system/role.vue
  24. 4 0
      src/pages/index/system/tag.vue
  25. 4 0
      src/pages/index/system/user.vue
  26. 30 2
      src/request/api/order.api.ts
  27. 4 0
      src/satisfaction/SendRecord.vue
  28. 4 0
      src/satisfaction/SurveyList.vue
  29. 4 0
      src/service/NotifyManageList.vue
  30. 4 0
      src/service/NotifyManageRecord.vue
  31. 123 0
      src/service/ReviewMediaPreview.vue
  32. 5 0
      src/service/ServiceItemsConfirm.vue
  33. 5 0
      src/service/ServiceItemsList.vue
  34. 5 0
      src/service/ServiceItemsSystem.vue
  35. 360 16
      src/service/SingleItemDetail.vue
  36. 340 0
      src/service/seeEvaluate.vue
  37. 4 0
      src/widgets/PatientCareRecordsWidget.vue
  38. 4 0
      src/widgets/PatientDiagnosisRecordsWidget.vue
  39. 4 0
      src/widgets/PatientFollowUpRecordsWidget.vue
  40. 4 0
      src/widgets/PatientHealthRecordsWidget.vue

BIN
src/assets/images/cancel.png


+ 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%;
 }

+ 93 - 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,53 @@ 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;//创建时间
+}
+
+export interface ApplyRecordModel{
+  id: number; // 主键ID
+  arrangeTime: string; // 预定时间
+  arrangeDuration: number; // 预定时长
+  arrangePeriod: string; // 服务时间段
+  applyTime: string; // 预约时间
+  updateTime: string; // 更新预约时间
+  cancelTime: string; // 取消预约时间
+  conditioningProgramSupplierId: number; // 供应商ID
+  conditioningProgramSupplierName: string; // 供应商名称
+  pieBy: string; // 派单人
+  pieTime: string; // 派单时间
+  operateTime: string; // 操作时间
+  status: string; // 服务状态
+}

+ 8 - 2
src/order/DispatchOrderPanel.vue

@@ -133,7 +133,10 @@ function calculateEndTime(order: OrderModel): string | null {
 function isOrderVerified(order: OrderModel): boolean {
   return props.orderType === 'offline' && order.type === 3;
 }
-
+// 判断订单是否取消预约派单(线下服务)
+function isOrderCancelPie(order: OrderModel): boolean {
+  return props.orderType === 'offline' && order.type === 4;
+}
 // 判断订单是否已发货(实体商品)
 function isOrderShipped(order: OrderModel): boolean {
   return props.orderType === 'physical' && order.type === 3;
@@ -171,6 +174,7 @@ const gridOptions = reactive<VxeGridProps<Institution>>({
     { field: 'detailAddress', title: '地址' },
     { field: 'phone', title: '联系电话' },
     { field: 'todayOrderQuantity', title: '当日订单数' },
+    { field: 'evaluateScore', title: '机构评分' },
   ],
   data: [],
   pagerConfig: {
@@ -1100,9 +1104,11 @@ defineExpose({
                 <!-- 已核销/已发货印章 -->
                 <!-- 线下服务:已核销状态显示已核销图标 -->
                 <!-- 实体商品:已发货状态显示已发货图标 -->
-                <div v-if="isOrderVerified(order) || isOrderShipped(order)" class="verified-badge">
+                <div v-if="isOrderVerified(order) || isOrderShipped(order) || isOrderCancelPie(order)" class="verified-badge">
                   <img v-if="isOrderVerified(order)" src="@/assets/images/verify.png" alt="已核销"
                     style="width: 100px; height: 100px;" />
+                  <img v-else-if="isOrderCancelPie(order)" src="@/assets/images/cancel.png" alt="已取消派单"
+                    style="width: 100px; height: 100px;" />
                   <img v-else-if="isOrderShipped(order)" src="@/assets/images/shipment.png" alt="已发货"
                     style="width: 100px; height: 100px;" />
                 </div>

+ 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,

+ 53 - 29
src/pages/index/system/institution.vue

@@ -1,30 +1,45 @@
 <script setup lang="ts">
 import InstitutionEdit from '@/components/InstitutionEdit.vue';
-import UserPassword from '@/components/UserPassword.vue';
-import UserPreview from '@/components/UserPreview.vue';
-import UserQRCode from '@/components/UserQRCode.vue';
 import { type InstitutionModel, type InstitutionQuery } from '@/model/system.model';
 
-import { branchMethod, institutionMethod, editInstitutionMethod, deleteInstitutionMethod } from '@/request/api/system.api';
+import { branchMethod, institutionMethod, deleteInstitutionMethod } from '@/request/api/system.api';
 import { usePagination, useRequest } from 'alova/client';
 import { notification } from 'ant-design-vue';
 
 import { VxeButton, type VxeFormListeners, type VxeFormProps, type VxeGridInstance, type VxeGridListeners, type VxeGridProps, VxeUI } from 'vxe-pc-ui';
 
-const { data: branch, loading: branchLoading } = useRequest(branchMethod);
-const organizationOptions = ref<{ label: string; value: string }[]>([
-  { label: 'liuzhi', value: '1' },
-  { label: 'alice', value: '2' },
-]);
+const { data: branch, loading: branchLoading } = useRequest(branchMethod(0, 1, 1));
 const model = shallowRef<InstitutionQuery>();
+function findDeptLabelById(id: any, nodes?: any[]): string | undefined {
+  if (!id || !Array.isArray(nodes)) return;
+  const target = String(id);
+  for (const n of nodes) {
+    if (n?.id !== undefined && String(n.id) === target) return n?.label;
+    const hit = findDeptLabelById(id, n?.children);
+    if (hit) return hit;
+  }
+}
 const searchFormProps = reactive<VxeFormProps<InstitutionQuery>>({
   titleWidth: 100,
   titleAlign: 'right',
   titleColon: true,
   data: {},
   items: [
-    { field: 'userName', title: '组织名称', span: 8, itemRender: { name: 'VxeSelect', props: { options: organizationOptions, optionProps: { value: 'value', label: 'label' } } } },
-    { field: 'nickName', title: '机构名称', span: 8, itemRender: { name: 'VxeInput' } },
+    {
+      field: 'orgName',
+      title: '组织名称',
+      span: 8,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    },
+    { field: 'name', title: '机构名称', span: 8, itemRender: { name: 'VxeInput' } },
     {
       span: 8,
       itemRender: {
@@ -39,13 +54,15 @@ const searchFormProps = reactive<VxeFormProps<InstitutionQuery>>({
 });
 const searchFormEmits: VxeFormListeners<InstitutionQuery> = {
   submit({ data }) {
-    model.value = { ...data };
+    const orgName = findDeptLabelById((data as any)?.orgName, branch.value as any) ?? (data as any)?.orgName;
+    model.value = { ...(data as any), orgName };
   },
   reset({ data }) {
     model.value = { ...data };
   },
 };
 
+const gridEvents: VxeGridListeners = {};
 const gridRef = ref<VxeGridInstance<InstitutionModel>>();
 const gridOptions = reactive<VxeGridProps<InstitutionModel>>({
   id: 'user-list',
@@ -55,6 +72,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,
@@ -92,7 +113,7 @@ const gridOptions = reactive<VxeGridProps<InstitutionModel>>({
           { content: '小程序码', status: 'primary', name: 'QRCode' },
         ],
         events: {
-          click({ row, rowIndex }, { name }) {
+          click({ row, rowIndex }: any, { name }: any) {
             let method;
             if (name === 'editInstitution') {
               method = editInstitution;
@@ -153,19 +174,23 @@ function editInstitution(model?: InstitutionModel, index?: number) {
     storage: true,
     slots: {
       default() {
-        return h(InstitutionEdit, <any>{
-          data: model,
-          onSubmit(data?: InstitutionModel) {
-            refresh(page.value);
-            VxeUI.modal.close(`institution-edit-modal`);
-          },
-        });
+        return h(
+          InstitutionEdit,
+          {
+            data: model,
+            onSubmit(data?: InstitutionModel) {
+              refresh(page.value);
+              VxeUI.modal.close(`institution-edit-modal`);
+            },
+          } as any
+        );
       },
     },
   });
 }
 
 function QRCode(model: InstitutionModel) {
+  console.log(model,111);
   const { name } = model;
   VxeUI.modal.open({
     title: `${name} 专属小程序码`,
@@ -178,9 +203,12 @@ function QRCode(model: InstitutionModel) {
     width: 256 + 12 * 2,
     slots: {
       default() {
-        return h(UserQRCode, <any>{
-          dataset: model,
-        });
+        return h(
+          UserQRCode,
+          {
+            dataset: model,
+          } as any
+        );
       },
     },
   });
@@ -202,12 +230,8 @@ function QRCode(model: InstitutionModel) {
       </vxe-grid>
     </main>
     <footer class="flex-none">
-      <vxe-pager
-        v-model:current-page="page"
-        v-model:page-size="pageSize"
-        :total="total"
-        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']"
-      />
+      <vxe-pager v-model:current-page="page" v-model:page-size="pageSize" :total="total"
+        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']" />
     </footer>
   </div>
 </template>

+ 52 - 29
src/pages/index/system/organization.vue

@@ -1,17 +1,23 @@
 <script setup lang="ts">
 import OrganizationManagement from '@/components/OrganizationManagement.vue';
 import type { OrganizationModel, OrganizationQuery } from '@/model/system.model';
-
 import { organizationMethod, deleteOrganizationMethod } from '@/request/api/system.api';
-import { usePagination } from 'alova/client';
+import { usePagination, useRequest } from 'alova/client';
 import { notification } from 'ant-design-vue';
 import PatientBelong from '@/components/PatientBelong.vue';
 import { type VxeFormListeners, type VxeFormProps, type VxeGridInstance, type VxeGridListeners, type VxeGridProps, VxeUI } from 'vxe-pc-ui';
-const organizationOptions = ref<{ label: string; value: string }[]>([
-  { label: 'liuzhi', value: '1' },
-  { label: 'alice', value: '2' },
-]);
+import { branchMethod } from '@/request/api/system.api';
 const model = shallowRef<OrganizationQuery>();
+const { data: branch, loading: branchLoading } = useRequest(branchMethod(0, 1, 1));
+function findDeptLabelById(id: any, nodes?: any[]): string | undefined {
+  if (!id || !Array.isArray(nodes)) return;
+  const target = String(id);
+  for (const n of nodes) {
+    if (n?.id !== undefined && String(n.id) === target) return n?.label;
+    const hit = findDeptLabelById(id, n?.children);
+    if (hit) return hit;
+  }
+}
 const searchFormProps = reactive<VxeFormProps<OrganizationQuery>>({
   titleWidth: 100,
   titleAlign: 'right',
@@ -20,8 +26,18 @@ const searchFormProps = reactive<VxeFormProps<OrganizationQuery>>({
     name: '',
   },
   items: [
-    { field: 'name', title: '组织名称', span: 8, itemRender: { name: 'VxeSelect', props: { options: organizationOptions, optionProps: { value: 'value', label: 'label' } } } },
-
+    {
+      field: 'name', title: '组织名称', span: 8,
+      itemRender: {
+        name: 'VxeTreeSelect',
+        props: {
+          loading: computed(() => branchLoading.value),
+          options: computed(() => branch.value),
+          optionProps: { value: 'id', label: 'label' },
+          clearable: true,
+        },
+      },
+    },
     {
       span: 16,
       itemRender: {
@@ -39,7 +55,8 @@ const searchFormProps = reactive<VxeFormProps<OrganizationQuery>>({
 });
 const searchFormEmits: VxeFormListeners<OrganizationQuery> = {
   submit({ data }) {
-    model.value = { ...data };
+    const name = findDeptLabelById((data as any)?.name, branch.value as any) ?? (data as any)?.name;
+    model.value = { ...(data as any), name };
   },
   reset({ data }) {
     model.value = { ...data };
@@ -55,6 +72,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,
@@ -160,13 +181,16 @@ function editOrganization(model?: OrganizationModel, index?: number) {
     height: 600,
     slots: {
       default() {
-        return h(OrganizationManagement, <any>{
-          data: model,
-          onSubmit(data: OrganizationModel) {
-            refresh(page.value);
-            VxeUI.modal.close(`organization-edit-modal`);
-          },
-        });
+        return h(
+          OrganizationManagement,
+          {
+            data: model,
+            onSubmit(data: OrganizationModel) {
+              refresh(page.value);
+              VxeUI.modal.close(`organization-edit-modal`);
+            },
+          } as any
+        );
       },
     },
   });
@@ -184,13 +208,16 @@ function setPatientBelong(model: OrganizationModel, index: number) {
     height: 600,
     slots: {
       default() {
-        return h(PatientBelong, <any>{
-          data: model,
-          onSubmit(data: OrganizationModel) {
-            refresh(page.value);
-            VxeUI.modal.close(`patient-belong-modal`);
-          },
-        });
+        return h(
+          PatientBelong,
+          {
+            data: model,
+            onSubmit(data: OrganizationModel) {
+              refresh(page.value);
+              VxeUI.modal.close(`patient-belong-modal`);
+            },
+          } as any
+        );
       },
     },
   });
@@ -212,12 +239,8 @@ function setPatientBelong(model: OrganizationModel, index: number) {
       </vxe-grid>
     </main>
     <footer class="flex-none">
-      <vxe-pager
-        v-model:current-page="page"
-        v-model:page-size="pageSize"
-        :total="total"
-        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']"
-      />
+      <vxe-pager v-model:current-page="page" v-model:page-size="pageSize" :total="total"
+        :layouts="['Home', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'End', 'Sizes', 'FullJump', 'Total']" />
     </footer>
   </div>
 </template>

+ 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,

+ 30 - 2
src/request/api/order.api.ts

@@ -1,5 +1,10 @@
 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, ApplyRecordModel
+} from '@/model/order.model';
 import request from '@/request/alova';
 
 // 线下服务  今日指派订单分页列表
@@ -163,4 +168,27 @@ 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$/, // 匹配失效源
+    }
+  );
+}
+
+// 获取线下服务的预约派单记录 	线下服务id
+export function getApplyRecordMethod(id: string) {
+  return request.Post<ApplyRecordModel[]>(
+    `/fdhb-pc/patientCrManage/getApplyRecord/${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 },

+ 360 - 16
src/service/SingleItemDetail.vue

@@ -1,10 +1,35 @@
 <script setup lang="ts">
 import type { SystemCwModel } from '@/model/care.model';
-import { computed } from 'vue';
+import type { ApplyRecordModel } from '@/model/order.model';
+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, getApplyRecordMethod } 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,100 @@ 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 applyRecordList = ref<ApplyRecordModel[]>([]);
+// 获取预约
+async function getApplyRecord() {
+  const id = (props.data as any)?.id;
+  if (!id) return;
+  const res = await getApplyRecordMethod(id);
+  if (!res) return;
+  console.log(res, "获取预约");
+  applyRecordList.value = res ?? [];
+  console.log(applyRecordList.value, "applyRecordList");
+}
+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(async () => {
+  await getEvaluateDetail();
+  await getApplyRecord();
+});
 </script>
 
 <template>
@@ -130,16 +247,13 @@ const sellTypeText: Record<string, string> = {
     <!-- 分账信息 -->
     <div class="info-section">
       <h3 class="info-title">分账信息</h3>
-      <vxe-table
-        class="split-account-table"
-        :data="mockSplitAccountList"
-        border
-      >
+      <vxe-table class="split-account-table" :data="mockSplitAccountList" border>
         <vxe-column field="profitSharingTime" title="分账时间" align="center" />
         <vxe-column field="conditioningProgramSupplierName" title="供应商" align="center" />
-        <vxe-column field="profitSharingStatus" title="分账状态" align="center" >
+        <vxe-column field="profitSharingStatus" title="分账状态" align="center">
           <template #default="{ row }">
-            {{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' : row.profitSharingStatus === '3' ? '分账异常' : '' }}
+            {{ row.profitSharingStatus === '1' ? '未分账' : row.profitSharingStatus === '2' ? '已分账' :
+              row.profitSharingStatus === '3' ? '分账异常' : '' }}
           </template>
         </vxe-column>
         <vxe-column field="profitSharing" title="分账比例" align="center">
@@ -147,7 +261,7 @@ const sellTypeText: Record<string, string> = {
             {{ row.profitSharing || '-' }}%
           </template>
         </vxe-column>
-        <vxe-column field="profitSharingAmount" title="预计分账金额" align="center" >
+        <vxe-column field="profitSharingAmount" title="预计分账金额" align="center">
           <template #default="{ row }">
             {{ row.profitSharingAmount ? row.profitSharingAmount + '元' : '' }}
           </template>
@@ -164,7 +278,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 +310,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;"
@@ -221,19 +335,41 @@ const sellTypeText: Record<string, string> = {
         暂无
       </div>
     </div>
-
+    <!-- 预约派单记录 线下服务才显示  1-实体商品 2-线下服务 3-线上权益-->
+    <div class="info-section" v-if="data?.sellType === '2'">
+      <h3 class="info-title">
+        预约派单记录
+      </h3>
+      <vxe-table :data="applyRecordList" border>
+        <vxe-column type="seq" title="序号" width="60" align="center" />
+        <vxe-column field="applyTime" title="预约时间" align="center" />
+        <vxe-column field="updateTime" title="最新修改预约时间" align="center" />
+        <vxe-column field="cancelTime" title="取消预约时间" align="center" />
+        <vxe-column field="arrangeTime" title="预约服务日期" align="center" />
+        <vxe-column field="arrangePeriod" title="预约服务时间段" align="center" />
+        <vxe-column field="pieTime" title="是否派单" align="center">
+          <template #default="{ row }">
+            {{ row.pieTime ? '是' : '否' }}
+          </template>
+        </vxe-column>
+        <vxe-column field="pieBy" title="派单员" align="center" />
+        <vxe-column field="pieTime" title="派单时间" align="center" />
+        <vxe-column field="conditioningProgramSupplierName" title="服务机构" align="center" />
+        <vxe-column field="status" title="服务状态" align="center" />
+      </vxe-table>
+    </div>
     <!-- 服务记录 线下服务才显示  1-实体商品 2-线下服务 3-线上权益-->
     <div class="info-section" v-if="data?.sellType === '2'">
       <h3 class="info-title">
         服务记录
         <span class="title-count">({{ data?.patientConditioningOfflines?.length || 0 }}/{{ data?.totalMeasure || 0
-        }})</span>
+          }})</span>
       </h3>
       <vxe-table :data="data?.patientConditioningOfflines" border>
         <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 +393,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 +482,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;
@@ -320,6 +656,7 @@ const sellTypeText: Record<string, string> = {
 
   /* 分账表格仅按内容自适应高度,避免一行数据时出现多余空白 */
   .split-account-table {
+
     :deep(.vxe-table--wrapper),
     :deep(.vxe-table) {
       height: auto !important;
@@ -333,6 +670,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' } },
     {