WXL
3 天以前 4d9da000fbe74d344e0e4580b138e79d4ad98ede
pages/ethicalReview/ethicalInfo.vue
@@ -4,8 +4,11 @@
    <view class="review-overview card">
      <view class="overview-header">
        <text class="title">伦理审查任务</text>
        <view class="status-badge" :class="reviewStatus">
          {{ statusText }}
        <view
          class="status-badge"
          :class="getStatusClass(caseInfo.receiveStatus)"
        >
          {{ getStatusText(caseInfo.receiveStatus) }}
        </view>
      </view>
      
@@ -14,22 +17,27 @@
        <view class="compact-info-item">
          <up-icon name="file-text" size="14" color="#909399" />
          <text class="compact-label">住院号</text>
          <text class="compact-value">{{ caseInfo.hospitalNo }}</text>
          <text class="compact-value">{{ caseInfo.inpatientno || "--" }}</text>
        </view>
        <view class="compact-info-item">
          <up-icon name="account" size="14" color="#909399" />
          <text class="compact-label">捐献者</text>
          <text class="compact-value">{{ caseInfo.donorName }}</text>
          <text class="compact-value">{{ caseInfo.name || "--" }}</text>
        </view>
        <view class="compact-info-item">
          <up-icon name="man" size="14" color="#909399" />
          <text class="compact-label">性别/年龄</text>
          <text class="compact-value">{{ caseInfo.gender }}/{{ caseInfo.age }}岁</text>
          <text class="compact-value"
            >{{ caseInfo.sex || "--" }}/{{ caseInfo.age || "--"
            }}{{ caseInfo.ageunit || "岁" }}</text
          >
        </view>
        <view class="compact-info-item">
          <up-icon name="heart" size="14" color="#909399" />
          <text class="compact-label">疾病诊断</text>
          <text class="compact-value">{{ caseInfo.diagnosis }}</text>
          <text class="compact-value">{{
            caseInfo.diagnosisname || "--"
          }}</text>
        </view>
      </view>
    </view>
@@ -51,6 +59,9 @@
          <view class="material-left">
            <up-icon :name="material.icon" :color="material.color" size="18" />
            <text class="file-name">{{ material.name }}</text>
            <view v-if="material.type" class="file-meta">
              <text class="file-type">{{ material.type || "文件" }}</text>
            </view>
          </view>
          <view class="material-right">
            <text class="file-size">{{ material.size }}</text>
@@ -64,6 +75,10 @@
    <view class="review-form card">
      <view class="section-header">
        <text class="section-title">审查意见</text>
        <view v-if="isTimeout" class="timeout-badge">
          <up-icon name="clock" size="16" />
          <text>已超时</text>
        </view>
      </view>
      <view class="form-content">
@@ -71,10 +86,11 @@
        <view class="form-group">
          <text class="form-label">审查结论</text>
          <u-radio-group 
            v-model="form.conclusion"
            v-model="form.expertconclusion"
            placement="column"
            activeColor="#007aff"
            @change="onConclusionChange"
            :disabled="isReadonly"
          >
            <u-radio
              v-for="option in conclusionOptions"
@@ -90,42 +106,14 @@
        <view class="form-group">
          <text class="form-label">详细意见</text>
          <u--textarea
            v-model="form.opinion"
            v-model="form.expertopinion"
            placeholder="请输入详细的审查意见和改进建议..."
            maxlength="1000"
            count
            :height="120"
            border="surround"
            :disabled="isReadonly"
          ></u--textarea>
        </view>
        <!-- 风险评估 -->
        <view class="form-group">
          <text class="form-label">风险评估</text>
          <view class="risk-assessment">
            <view class="risk-item">
              <text class="risk-label">受试者风险等级</text>
              <view class="risk-slider-compact">
                <view class="risk-levels">
                  <text class="level-label" :class="{ active: form.riskLevel >= 1 }">低</text>
                  <text class="level-label" :class="{ active: form.riskLevel >= 2 }">中低</text>
                  <text class="level-label" :class="{ active: form.riskLevel >= 3 }">中</text>
                  <text class="level-label" :class="{ active: form.riskLevel >= 4 }">中高</text>
                  <text class="level-label" :class="{ active: form.riskLevel >= 5 }">高</text>
                </view>
                <slider
                  v-model="form.riskLevel"
                  min="1"
                  max="5"
                  step="1"
                  activeColor="#f56c6c"
                  backgroundColor="#e4e7ed"
                  block-color="#f56c6c"
                  block-size="20"
                />
              </view>
            </view>
          </view>
        </view>
        <!-- 签名确认 -->
@@ -142,31 +130,158 @@
      </view>
    </view>
    <!-- 操作按钮 -->
    <view class="action-bar-compact">
      <button
        class="action-btn save-btn"
        @tap="saveDraft"
    <!-- 签名确认区域 -->
    <view class="signature-section card">
      <view class="section-header">
        <text class="section-title">专家签名确认</text>
        <view v-if="isTimeout" class="timeout-badge">
          <up-icon name="clock" size="16" />
          <text>已超时,不可操作</text>
        </view>
      </view>
        
      <view class="signature-content">
        <!-- 显示已签名图片 -->
        <view v-if="signatureData.signatureUrl" class="signed-preview">
          <image
            :src="signatureData.signatureUrl"
            mode="aspectFit"
            class="signature-image"
            @tap="previewSignature"
          />
          <view class="signature-info">
            <text class="signature-name">签名人:{{ expertInfo.name }}</text>
            <text class="signature-time">{{
              signatureData.signatureTime
            }}</text>
            <view class="signature-actions" v-if="!isReadonly">
              <button class="re-sign-btn" @tap="removeSignature">
                <u-icon name="photo" size="16" />
                <text>重新签名</text>
              </button>
            </view>
          </view>
        </view>
        <!-- 添加签名按钮 -->
        <view
          v-else
          class="signature-upload"
          @tap="openSignaturePanel"
          v-if="!isReadonly"
      >
          <view class="signature-upload-area">
            <up-icon name="edit-pen" size="48" color="#c0c4cc" />
            <text class="upload-hint">点击进行手写签名</text>
            <text class="upload-tip">签名将作为重要凭证</text>
          </view>
        </view>
        <!-- 超时状态下显示提示 -->
        <view
          v-if="isReadonly && !signatureData.signatureUrl"
          class="signature-disabled"
        >
          <view class="signature-disabled-area">
            <up-icon name="close-circle" size="48" color="#dcdfe6" />
            <text class="disabled-hint">当前任务已超时</text>
            <text class="disabled-tip">无法进行签名操作</text>
          </view>
        </view>
      </view>
    </view>
    <!-- 操作按钮 -->
    <view class="action-bar-compact" v-if="!isReadonly">
      <button class="action-btn save-btn" @tap="saveDraft" v-if="showSaveBtn">
        <up-icon name="file-text" size="16" color="#606266" />
        <text>保存草稿</text>
      </button>
      <button 
        class="action-btn submit-btn"
        @tap="submitReview"
        :disabled="!canSubmit"
        v-if="showSubmitBtn"
      >
        <up-icon name="checkmark" size="16" color="#fff" />
        <text>提交审查</text>
        <text>{{ submitBtnText }}</text>
      </button>
    </view>
    <!-- 只读状态提示 -->
    <view v-if="isReadonly" class="readonly-tip card">
      <up-icon name="info-circle" size="20" color="#fa8c16" />
      <text>当前任务已超时,仅可查看,不可操作</text>
    </view>
    <!-- 优化的签名弹窗组件 -->
    <u-popup
      :show="showSignaturePanel"
      @close="closeSignaturePanel"
      mode="bottom"
      :round="20"
      :closeable="true"
      closeIcon="close"
    >
      <view class="signature-modal">
        <view class="modal-header">
          <text class="modal-title">手写签名</text>
          <text class="modal-subtitle">请在下方区域进行签名</text>
        </view>
        <view class="signature-canvas-container">
          <!-- 手写签名画布 -->
          <canvas
            canvas-id="signatureCanvas"
            class="signature-canvas"
            :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
            @touchstart="onTouchStart"
            @touchmove="onTouchMove"
            @touchend="onTouchEnd"
            disable-scroll
          ></canvas>
          <!-- 操作按钮 -->
          <view class="canvas-actions">
            <button class="action-btn clear-btn" @tap="clearCanvas">
              <up-icon name="trash" size="20" />
              <text>清空</text>
            </button>
            <button
              class="action-btn redo-btn"
              @tap="undoLastStroke"
              :disabled="!canUndo"
            >
              <up-icon name="play-left" size="20" />
              <text>撤销</text>
            </button>
            <button
              class="action-btn confirm-btn"
              @tap="confirmSignature"
              :disabled="!hasSignature"
            >
              <up-icon name="checkmark" size="20" />
              <text>确认签名</text>
            </button>
          </view>
          <!-- 签名预览 -->
          <view v-if="tempSignatureData" class="signature-preview">
            <text class="preview-title">签名预览</text>
            <image
              :src="tempSignatureData"
              mode="aspectFit"
              class="preview-image"
            />
          </view>
        </view>
      </view>
    </u-popup>
    <!-- 提交确认弹窗 -->
    <u-modal
      :show="showSubmitModal"
      title="确认提交"
      content="确定要提交审查意见吗?提交后将无法修改。"
      :title="modalTitle"
      :content="modalContent"
      showCancelButton
      @confirm="confirmSubmit"
      @cancel="showSubmitModal = false"
@@ -175,114 +290,943 @@
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { ref, computed, reactive, onMounted, nextTick } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import dayjs from "dayjs";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const dict = ref({});
// 响应式数据
const caseInfo = ref({
  hospitalNo: "D230415",
  donorName: "张某某",
  gender: "男",
  age: "45",
  diagnosis: "终末期肝病"
  hospitalNo: "",
  donorName: "",
  gender: "",
  age: "",
  diagnosis: "",
  receiveStatus: "0",
  endtime: "",
});
const materials = ref([
  { id: 1, name: "捐献者知情同意书.pdf", icon: "file-text", color: "#f56c6c", size: "2.3MB" },
  { id: 2, name: "医学评估报告.docx", icon: "file-text", color: "#1890ff", size: "1.1MB" },
  { id: 3, name: "实验室检查结果.xlsx", icon: "file-text", color: "#52c41a", size: "0.8MB" },
  { id: 4, name: "影像学资料.jpg", icon: "photo", color: "#fa8c16", size: "3.2MB" }
]);
const id = ref(null);
const fcid = ref(null);
const materials = ref([]);
const form = ref({
  conclusion: "",
  opinion: "",
  riskLevel: 3
  expertconclusion: "", // 专家结论
  expertopinion: "", // 专家意见
});
const expertInfo = ref({
  name: "孔心涓",
  title: "主委专家"
  name: "",
  title: "",
});
const reviewStatus = ref("pending");
const showSubmitModal = ref(false);
const modalTitle = ref("确认提交");
const modalContent = ref("确定要提交审查意见吗?提交后将无法修改。");
// 计算属性
const statusText = computed(() => {
  const statusMap = {
    pending: "待审查",
    drafted: "草稿",
    submitted: "已提交"
  };
  return statusMap[reviewStatus.value];
});
const canSubmit = computed(() => {
  return form.value.conclusion !== "" && form.value.opinion.trim().length > 10;
});
const currentTime = computed(() => {
  return new Date().toLocaleString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit'
  return new Date().toLocaleString("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  });
});
const conclusionOptions = ref([
  { label: "同意", value: "approved" },
  { label: "修改后同意", value: "approved_with_modifications" },
  { label: "修改后重审", value: "re-review" },
  { label: "不同意", value: "disapproved" }
  { label: "同意", value: "1" }, // 对应receiveStatus 5-完成
  { label: "驳回", value: "2" }, // 对应receiveStatus 6-驳回
]);
// 方法
const loadReviewData = (reviewId) => {
  console.log("加载审查数据:", reviewId);
// 签名相关数据
const showSignaturePanel = ref(false);
const signatureData = reactive({
  signatureUrl: "",
  signatureTime: "",
  fileName: "",
  serverData: null,
});
// 手写签名相关
const canvasWidth = 650;
const canvasHeight = 300;
let ctx = null;
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let strokeHistory = [];
const tempSignatureData = ref("");
const canUndo = computed(() => strokeHistory.length > 0);
const hasSignature = computed(() => tempSignatureData.value !== "");
// 上传配置
const uploadConfig = reactive({
  uploadUrl: "/api/common/upload",
  extraParams: {
    caseNo: "",
    expertId: "",
    expertName: "",
    type: "ethics_review_signature",
    bizType: "expert_review",
  },
});
// 按钮显示控制
const showSaveBtn = ref(true);
const showSubmitBtn = ref(true);
const submitBtnText = ref("提交审查");
// 状态转换
const getStatusText = (status) => {
  const statusMap = {
    0: "待接收",
    1: "未接收",
    2: "已接收",
    3: "超时",
    4: "中止",
    5: "完成",
    6: "驳回",
  };
  return statusMap[status] || "未知状态";
};
const getStatusClass = (status) => {
  const classMap = {
    0: "pending",
    1: "pending",
    2: "pending",
    3: "submitted",
    4: "submitted",
    5: "success",
    6: "error",
  };
  return classMap[status] || "pending";
};
// 是否超时
const isTimeout = computed(() => {
  return caseInfo.value.receiveStatus === "3";
});
// 是否只读模式
const isReadonly = computed(() => {
  return isTimeout.value;
});
// 是否可以提交
const canSubmit = computed(() => {
  if (isReadonly.value) return false;
  return (
    form.value.expertconclusion !== "" &&
    form.value.expertopinion.trim().length > 0 &&
    signatureData.signatureUrl !== "" &&
    signatureData.signatureUrl.startsWith("http") // 确保是完整的URL路径
  );
});
// 生命周期
onLoad(async (options) => {
  id.value = options.id;
  fcid.value = options.fcid;
  if (fcid.value) {
    await loadCaseData(fcid.value);
  }
});
onShow(() => {
  // 页面显示时检查用户登录状态
  const userInfo = uni.getStorageSync("userInfo");
  if (userInfo) {
    expertInfo.value.name = userInfo.nickName || userInfo.userName || "";
  }
});
// 加载案件数据
const loadCaseData = async (id) => {
  try {
    uni.showLoading({ title: "加载中..." });
    // 构建查询参数
    const params = {
      fcid: id,
      pageNum: 1,
      pageSize: 1,
    };
    const res = await uni.$uapi.get(
      `/project/ethicalreviewopinions/listnew`,
      params,
    );
    if (res.code === 200 && res.rows && res.rows.length > 0) {
      const data = res.rows[0];
      console.log("加载的审查数据:", data);
      // 设置案件信息
      caseInfo.value = {
        ...caseInfo.value,
        inpatientno: data.inpatientno,
        name: data.name,
        sex: data.sex,
        age: data.age,
        ageunit: data.ageunit,
        expertType: data.expertType,
        diagnosisname: data.diagnosisname,
        receiveStatus: data.receiveStatus || "0",
        endtime: data.endtime || "",
        caseNo: data.caseNo || "",
      };
      // 设置表单数据
      if (data.expertconclusion) {
        form.value.expertconclusion = data.expertconclusion.toString();
      }
      if (data.expertopinion) {
        form.value.expertopinion = data.expertopinion;
      }
      // 设置专家信息
      if (data.expertname) {
        expertInfo.value.name = data.expertname;
      } else {
        // 从用户信息获取
        const userInfo = uni.getStorageSync("userInfo");
        if (userInfo) {
          expertInfo.value.name = userInfo.nickName || userInfo.userName || "";
        }
      }
      // 设置签名
      if (data.sigin) {
        // 检查签名URL是否是完整路径
        if (data.sigin.startsWith("http")) {
          signatureData.signatureUrl = data.sigin;
        } else {
          // 如果不是完整路径,可能需要拼接基础URL
          signatureData.signatureUrl = `/api${
            data.sigin.startsWith("/") ? "" : "/"
          }${data.sigin}`;
        }
        signatureData.signatureTime = data.conclusiontime || "";
      }
      // 解析附件
      if (data.filePatch) {
        parseAnnexFiles(data.filePatch);
      }
      // 检查是否超时
      checkTimeoutStatus(data);
      // 根据状态控制按钮显示
      updateButtonStatus(data.receiveStatus);
      // 更新上传参数
      uploadConfig.extraParams.caseNo = data.caseNo || "";
      uploadConfig.extraParams.expertName = expertInfo.value.name;
    } else {
      uni.showToast({
        title: res.msg || "未找到审查数据",
        icon: "none",
      });
    }
  } catch (error) {
    if (error.message === "未登录") {
      // ✅ 什么都不做,拦截器已经处理
      return;
    }
    uni.showToast({ title: "加载失败", icon: "none" });
  } finally {
    uni.hideLoading();
  }
};
// 检查是否超时
const checkTimeoutStatus = (data) => {
  if (data.receiveStatus === "3") {
    // 状态已经是超时
    return;
  }
  // 如果有截止时间,检查是否超过当前时间
  if (data.endtime) {
    const endTime = new Date(data.endtime);
    const now = new Date();
    if (now > endTime) {
      // 标记为超时状态
      caseInfo.value.receiveStatus = "3";
      updateButtonStatus("3");
    }
  }
};
// 解析附件文件
const parseAnnexFiles = (filePatch) => {
  if (!filePatch) return;
  try {
    // 解析JSON字符串
    let fileList = [];
    // 检查是否是JSON字符串格式
    if (filePatch.startsWith("[") && filePatch.endsWith("]")) {
      // 处理转义字符
      const cleanJson = filePatch.replace(/\\"/g, '"');
      fileList = JSON.parse(cleanJson);
    } else if (filePatch.includes("fileName")) {
      // 尝试直接解析
      fileList = JSON.parse(filePatch);
    } else {
      // 旧格式处理
      const oldFileList = filePatch.split(";").filter((item) => item.trim());
      fileList = oldFileList.map((file) => ({
        fileName: file.split("/").pop() || `附件`,
        path: file,
        fileUrl: file,
      }));
    }
    // 转换为需要的格式
    materials.value = fileList.map((file, index) => {
      const fileName = file.fileName || `附件${index + 1}`;
      const fileUrl = file.fileUrl || file.path || file.url || "";
      const ext = fileName.split(".").pop().toLowerCase();
      let icon = "file-text";
      let color = "#909399";
      if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext)) {
        icon = "photo";
        color = "#fa8c16";
      } else if (["doc", "docx"].includes(ext)) {
        icon = "file-text";
        color = "#1890ff";
      } else if (["xls", "xlsx", "csv"].includes(ext)) {
        icon = "file-text";
        color = "#52c41a";
      } else if (["pdf"].includes(ext)) {
        icon = "file-text";
        color = "#f56c6c";
      } else if (["txt", "text"].includes(ext)) {
        icon = "file-text";
        color = "#909399";
      } else if (["zip", "rar", "7z"].includes(ext)) {
        icon = "folder";
        color = "#722ed1";
      }
      return {
        id: file.infoid || index + 1,
        name: fileName,
        icon: icon,
        color: color,
        size: "--",
        url: fileUrl,
        type: ext,
        createTime: file.createTime || "",
      };
    });
  } catch (error) {
    console.error("解析附件失败:", error, filePatch);
    // 回退到旧格式处理
    const fileList = filePatch.split(";").filter((item) => item.trim());
    materials.value = fileList.map((file, index) => {
      const fileName = file.split("/").pop() || `附件${index + 1}`;
      const ext = fileName.split(".").pop().toLowerCase();
      let icon = "file-text";
      let color = "#909399";
      if (["jpg", "jpeg", "png", "gif", "bmp"].includes(ext)) {
        icon = "photo";
        color = "#fa8c16";
      } else if (["doc", "docx"].includes(ext)) {
        icon = "file-text";
        color = "#1890ff";
      } else if (["xls", "xlsx"].includes(ext)) {
        icon = "file-text";
        color = "#52c41a";
      } else if (["pdf"].includes(ext)) {
        icon = "file-text";
        color = "#f56c6c";
      }
      return {
        id: index + 1,
        name: fileName,
        icon: icon,
        color: color,
        size: "--",
        url: file,
      };
    });
  }
};
// 更新按钮状态
const updateButtonStatus = (status) => {
  const statusNum = status;
  switch (statusNum) {
    case "3": // 超时
    case "4": // 中止
    case "5": // 已完成
    case "6": // 已驳回
      showSaveBtn.value = false;
      showSubmitBtn.value = false;
      break;
    default:
      showSaveBtn.value = true;
      showSubmitBtn.value = true;
      submitBtnText.value = "提交审查";
  }
};
// 打开签名面板
const openSignaturePanel = () => {
  if (isReadonly.value) {
    uni.showToast({
      title: "当前任务已超时,不可操作",
      icon: "none",
    });
    return;
  }
  showSignaturePanel.value = true;
  nextTick(() => {
    initCanvas();
  });
};
// 关闭签名面板
const closeSignaturePanel = () => {
  showSignaturePanel.value = false;
  clearCanvas();
};
// 初始化画布
const initCanvas = () => {
  ctx = uni.createCanvasContext("signatureCanvas");
  clearCanvas();
};
// 清空画布
const clearCanvas = () => {
  if (!ctx) return;
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  ctx.setStrokeStyle("#000000");
  ctx.setLineWidth(3);
  ctx.setLineCap("round");
  ctx.setLineJoin("round");
  ctx.draw();
  strokeHistory = [];
  tempSignatureData.value = "";
};
// 撤销上一步
const undoLastStroke = () => {
  if (strokeHistory.length === 0) return;
  // 移除最后一步
  strokeHistory.pop();
  // 重新绘制
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  ctx.setStrokeStyle("#000000");
  ctx.setLineWidth(3);
  ctx.setLineCap("round");
  ctx.setLineJoin("round");
  // 绘制历史笔迹
  strokeHistory.forEach((stroke) => {
    ctx.beginPath();
    ctx.moveTo(stroke.startX, stroke.startY);
    ctx.lineTo(stroke.endX, stroke.endY);
    ctx.stroke();
  });
  ctx.draw();
  // 更新预览
  if (strokeHistory.length === 0) {
    tempSignatureData.value = "";
  } else {
    getCanvasImage();
  }
};
// 触摸开始
const onTouchStart = (e) => {
  if (isReadonly.value) return;
  isDrawing = true;
  const touch = e.touches[0];
  lastX = touch.x;
  lastY = touch.y;
  ctx.beginPath();
  ctx.moveTo(lastX, lastY);
};
// 触摸移动
const onTouchMove = (e) => {
  if (!isDrawing || isReadonly.value) return;
  const touch = e.touches[0];
  const currentX = touch.x;
  const currentY = touch.y;
  ctx.lineTo(currentX, currentY);
  ctx.stroke();
  ctx.draw(true);
  // 保存笔迹到历史记录
  strokeHistory.push({
    startX: lastX,
    startY: lastY,
    endX: currentX,
    endY: currentY,
  });
  lastX = currentX;
  lastY = currentY;
};
// 触摸结束
const onTouchEnd = () => {
  if (!isDrawing) return;
  isDrawing = false;
  ctx.closePath();
  // 更新预览
  getCanvasImage();
};
// 获取画布图片
const getCanvasImage = () => {
  uni.canvasToTempFilePath({
    canvasId: "signatureCanvas",
    success: (res) => {
      tempSignatureData.value = res.tempFilePath;
    },
    fail: (err) => {
      console.error("获取画布图片失败:", err);
    },
  });
};
// 确认签名
const confirmSignature = async () => {
  if (!tempSignatureData.value) {
    uni.showToast({
      title: "请先签名",
      icon: "none",
    });
    return;
  }
  try {
    uni.showLoading({ title: "保存签名中..." });
    // 上传签名文件
    const uploadResult = await uploadSignature(tempSignatureData.value);
    if (uploadResult) {
      // 更新签名数据
      signatureData.signatureUrl = uploadResult.url;
      signatureData.signatureTime = new Date().toLocaleString("zh-CN");
      signatureData.fileName =
        uploadResult.originalFilename || `signature_${Date.now()}.png`;
      signatureData.serverData = uploadResult;
      // 保存到本地
      saveSignatureToLocal(uploadResult);
      uni.hideLoading();
      uni.showToast({
        title: "签名保存成功",
        icon: "success",
      });
      showSignaturePanel.value = false;
      clearCanvas();
    } else {
      uni.hideLoading();
      uni.showToast({
        title: "签名上传失败",
        icon: "none",
      });
    }
  } catch (error) {
    console.error("签名上传失败:", error);
    uni.hideLoading();
    uni.showToast({
      title: "签名上传失败",
      icon: "none",
    });
  }
};
// 上传签名文件方法
const uploadSignature = (filePath) => {
  return new Promise((resolve, reject) => {
    const token = uni.getStorageSync("token");
    // 获取用户信息
    const userInfo = uni.getStorageSync("userInfo");
    const expertName =
      userInfo?.nickName || userInfo?.userName || expertInfo.value.name;
    uni.uploadFile({
      url: "/api/common/upload",
      filePath: filePath,
      name: "file",
      header: {
        Authorization: `Bearer ${token}`,
      },
      formData: {
        // 添加额外参数,参考附件组件
        bizType: "expert_review_signature",
        caseNo: caseInfo.value.caseNo || "",
        expertName: expertName,
        uploadType: "signature",
      },
      success: (res) => {
        if (res.statusCode === 200) {
          const data = JSON.parse(res.data);
          console.log("签名上传成功:", data);
          if (data.code === 200) {
            resolve({
              url: data.url,
              fileName: data.fileName,
              newFileName: data.newFileName,
              originalFilename: data.originalFilename,
              filePath: data.filePath || data.fileName,
              size: data.size,
            });
          } else {
            reject(new Error(data.msg || "上传失败"));
          }
        } else {
          reject(new Error(`上传失败,状态码: ${res.statusCode}`));
        }
      },
      fail: (err) => {
        reject(err);
      },
    });
  });
};
// 保存到本地
const saveSignatureToLocal = (uploadData) => {
  try {
    const signatureInfo = {
      signatureUrl: signatureData.signatureUrl,
      signatureTime: signatureData.signatureTime,
      fileName: signatureData.fileName,
      uploadData: uploadData, // 保存上传返回的数据
      caseNo: caseInfo.value.caseNo,
      timestamp: Date.now(),
    };
    uni.setStorageSync("expert_review_signature", signatureInfo);
  } catch (error) {
    console.error("保存签名到本地失败:", error);
  }
};
// 删除签名
const removeSignature = () => {
  if (isReadonly.value) {
    uni.showToast({
      title: "当前任务已超时,不可操作",
      icon: "none",
    });
    return;
  }
  uni.showModal({
    title: "提示",
    content: "确定要删除签名吗?",
    success: (res) => {
      if (res.confirm) {
        signatureData.signatureUrl = "";
        signatureData.signatureTime = "";
        signatureData.fileName = "";
        signatureData.serverData = null;
        uni.removeStorageSync("expert_review_signature");
      }
    },
  });
};
// 预览签名
// 预览签名
const previewSignature = () => {
  if (signatureData.signatureUrl) {
    // 检查URL是否需要拼接基础路径
    let previewUrl = signatureData.signatureUrl;
    if (!previewUrl.startsWith("http")) {
      // 可能需要拼接服务器基础URL
      previewUrl = `${userStore?.baseUrlHt || ""}${previewUrl}`;
    }
    uni.previewImage({
      urls: [previewUrl],
    });
  }
};
// 预览材料
// 预览材料
const previewMaterial = (material) => {
  if (material.url) {
    uni.showLoading({ title: "加载中..." });
    const fileExt =
      material.type || material.url.split(".").pop().toLowerCase();
    // 判断文件类型
    if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(fileExt)) {
      // 图片直接预览
      uni.previewImage({
        urls: [material.url],
        current: 0,
        success: () => {
          console.log("图片预览成功");
        },
        fail: (err) => {
          console.error("图片预览失败:", err);
          uni.showToast({
            title: "图片加载失败",
            icon: "none",
          });
        },
        complete: () => {
          uni.hideLoading();
        },
      });
    } else if (
      ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt"].includes(
        fileExt,
      )
    ) {
      // 文档文件下载后打开
      uni.downloadFile({
        url: material.url,
        success: (res) => {
          if (res.statusCode === 200) {
            const filePath = res.tempFilePath;
            uni.openDocument({
              filePath: filePath,
              showMenu: true,
              fileType: fileExt === "pdf" ? "pdf" : "",
              success: () => {
                console.log("打开文档成功");
              },
              fail: (err) => {
                console.error("打开文档失败:", err);
                uni.showToast({
                  title: "无法打开该文件",
                  icon: "none",
                });
              },
            });
          }
        },
        fail: (err) => {
          console.error("下载文件失败:", err);
          uni.showToast({
            title: "文件下载失败",
            icon: "none",
          });
        },
        complete: () => {
          uni.hideLoading();
        },
      });
    } else {
      // 其他文件类型
      uni.showToast({
        title: `暂不支持预览${fileExt}格式文件`,
        icon: "none",
      });
      uni.hideLoading();
    }
  } else {
  uni.showToast({
    title: `预览: ${material.name}`,
    icon: "none"
      icon: "none",
  });
  }
};
const onConclusionChange = (value) => {
  console.log("选中结论:", value);
};
const saveDraft = () => {
// 保存草稿
const saveDraft = async () => {
  if (isReadonly.value) {
  uni.showToast({
    title: "草稿保存成功",
    icon: "success"
      title: "当前任务已超时,不可操作",
      icon: "none",
  });
  reviewStatus.value = "drafted";
    return;
  }
  if (!validateForm(true)) return;
  try {
    uni.showLoading({ title: "保存中..." });
    const submitData = {
      fcid: fcid.value,
      expertconclusion: form.value.expertconclusion,
      expertopinion: form.value.expertopinion,
      sigin: signatureData.signatureUrl, // 使用上传后的完整路径
      receiveStatus: "2", // 已接收状态
      conclusiontime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
};
    const res = await uni.$uapi.post(
      "/project/ethicalreviewopinions/edit",
      submitData,
    );
    if (res.code === 200) {
      uni.showToast({
        title: "保存成功",
        icon: "success",
      });
      // 更新本地状态
      caseInfo.value.receiveStatus = "2";
      updateButtonStatus("2");
    } else {
      uni.showToast({
        title: res.msg || "保存失败",
        icon: "none",
      });
    }
  } catch (error) {
    console.error("保存草稿失败:", error);
    uni.showToast({
      title: "保存失败",
      icon: "none",
    });
  } finally {
    uni.hideLoading();
  }
};
// 提交审查
const submitReview = () => {
  console.log(1);
  if (isReadonly.value) {
    uni.showToast({
      title: "当前任务已超时,不可操作",
      icon: "none",
    });
    return;
  }
  console.log(2);
  if (!validateForm()) return;
  modalTitle.value = "确认提交";
  modalContent.value = "确定要提交审查意见吗?提交后将无法修改。";
  showSubmitModal.value = true;
};
const confirmSubmit = () => {
// 确认提交
const confirmSubmit = async () => {
  try {
  uni.showLoading({ title: "提交中..." });
    console.log(caseInfo.value, "form.value");
  
  setTimeout(() => {
    uni.hideLoading();
    const submitData = {
      id: fcid.value,
      expertconclusion: form.value.expertconclusion,
      expertopinion: form.value.expertopinion,
      sigin: signatureData.signatureUrl,
      expertType: caseInfo.value.expertType,
      // 根据结论设置状态
      receiveStatus: form.value.expertconclusion == "1" ? "5" : "6", // 5-完成, 6-驳回
      conclusiontime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
    };
    const res = await uni.$uapi.post(
      "/project/ethicalreviewopinions/edit",
      submitData,
    );
    if (res.code === 200) {
    uni.showToast({
      title: "审查意见提交成功",
      icon: "success"
        title: "提交成功",
        icon: "success",
        duration: 2000,
    });
    reviewStatus.value = "submitted";
      // 更新本地状态
      caseInfo.value.receiveStatus = submitData.receiveStatus;
      updateButtonStatus(submitData.receiveStatus);
    showSubmitModal.value = false;
    
    setTimeout(() => {
      uni.navigateBack();
    }, 1500);
  }, 2000);
    } else {
      uni.showToast({
        title: res.msg || "提交失败",
        icon: "none",
      });
    }
  } catch (error) {
    console.error("提交失败:", error);
    uni.showToast({
      title: "提交失败",
      icon: "none",
    });
  } finally {
    uni.hideLoading();
  }
};
// 表单验证
const validateForm = (isDraft = false) => {
  if (!form.value.expertconclusion && !isDraft) {
    uni.showToast({
      title: "请选择审查结论",
      icon: "none",
    });
    return false;
  }
  console.log(3);
  if (!form.value.expertopinion.trim() && !isDraft) {
    uni.showToast({
      title: "请输入审查意见",
      icon: "none",
    });
    return false;
  }
  console.log(signatureData, "signatureData");
  if (!signatureData.signatureUrl && !isDraft) {
    uni.showToast({
      title: "请进行手写签名",
      icon: "none",
    });
    return false;
  }
  return true;
};
</script>
@@ -325,13 +1269,17 @@
          background: #fff2e8;
          color: #fa8c16;
        }
        &.drafted {
        &.submitted {
          background: #e6f7ff;
          color: #1890ff;
        }
        &.submitted {
        &.success {
          background: #f6ffed;
          color: #52c41a;
        }
        &.error {
          background: #fff1f0;
          color: #ff4d4f;
        }
      }
    }
@@ -363,7 +1311,46 @@
      }
    }
  }
  .material-left {
    display: flex;
    align-items: center;
    flex: 1;
    .file-info {
      margin-left: 16rpx;
      display: flex;
      flex-direction: column;
    }
    .file-name {
      font-size: 26rpx;
      color: #303133;
      max-width: 400rpx;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .file-meta {
      display: flex;
      align-items: center;
      gap: 8rpx;
      margin-top: 4rpx;
      .file-type {
        font-size: 20rpx;
        color: #909399;
        background: #f0f2f5;
        padding: 2rpx 8rpx;
        border-radius: 4rpx;
      }
      .file-time {
        font-size: 20rpx;
        color: #909399;
      }
    }
  }
  .materials-section {
    padding: 24rpx;
@@ -434,6 +1421,30 @@
  .review-form {
    padding: 24rpx;
    .section-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 20rpx;
      .section-title {
        font-size: 28rpx;
        font-weight: 600;
        color: #303133;
      }
      .timeout-badge {
        display: flex;
        align-items: center;
        gap: 4rpx;
        padding: 4rpx 12rpx;
        background: #fff2e8;
        border-radius: 12rpx;
        font-size: 22rpx;
        color: #fa8c16;
      }
    }
    .form-group {
      margin-bottom: 32rpx;
@@ -443,36 +1454,6 @@
        font-weight: 600;
        color: #303133;
        margin-bottom: 20rpx;
      }
    }
    .risk-assessment {
      .risk-item {
        .risk-label {
          display: block;
          font-size: 26rpx;
          color: #606266;
          margin-bottom: 16rpx;
        }
        .risk-slider-compact {
          .risk-levels {
            display: flex;
            justify-content: space-between;
            margin-bottom: 12rpx;
            .level-label {
              font-size: 22rpx;
              color: #c0c4cc;
              transition: color 0.3s;
              &.active {
                color: #f56c6c;
                font-weight: 500;
              }
            }
          }
        }
      }
    }
@@ -501,7 +1482,7 @@
  .action-bar-compact {
    position: fixed;
    bottom: 96rpx;
    bottom: 0rpx;
    left: 0;
    right: 0;
    display: flex;
@@ -529,7 +1510,7 @@
      }
      &.submit-btn {
        background: linear-gradient(135deg, #0f95b0, #89C4C1) !important;
        background: linear-gradient(135deg, #0f95b0, #89c4c1) !important;
        color: #fff;
        &:disabled {
@@ -540,6 +1521,271 @@
      &:active:not(:disabled) {
        transform: scale(0.98);
      }
    }
  }
  .readonly-tip {
    display: flex;
    align-items: center;
    gap: 8rpx;
    padding: 20rpx 24rpx;
    font-size: 24rpx;
    color: #fa8c16;
  }
}
.signature-section {
  margin-top: 20rpx;
  padding: 24rpx;
  background: #fff;
  border-radius: 16rpx;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
  .section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20rpx;
    .section-title {
      font-size: 28rpx;
      font-weight: 600;
      color: #333;
    }
    .timeout-badge {
      display: flex;
      align-items: center;
      gap: 4rpx;
      padding: 4rpx 12rpx;
      background: #fff2e8;
      border-radius: 12rpx;
      font-size: 22rpx;
      color: #fa8c16;
    }
  }
}
.signature-content {
  .signed-preview {
    display: flex;
    align-items: center;
    padding: 20rpx;
    background: #f8f9fa;
    border-radius: 12rpx;
    gap: 20rpx;
    .signature-image {
      width: 200rpx;
      height: 100rpx;
      border: 1rpx solid #dcdfe6;
      border-radius: 8rpx;
      background: #fff;
    }
    .signature-info {
      flex: 1;
      .signature-name {
        display: block;
        font-size: 26rpx;
        color: #333;
        margin-bottom: 8rpx;
        font-weight: 500;
      }
      .signature-time {
        display: block;
        font-size: 24rpx;
        color: #666;
        margin-bottom: 8rpx;
      }
      .signature-actions {
        margin-top: 12rpx;
        .re-sign-btn {
          padding: 8rpx 16rpx;
          border: 1rpx solid #dcdfe6;
          background: #fff;
          border-radius: 8rpx;
          font-size: 24rpx;
          color: #f56c6c;
          display: flex;
          align-items: center;
          gap: 6rpx;
          &:active {
            background: #f5f7fa;
          }
        }
      }
    }
  }
  .signature-upload {
    .signature-upload-area {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 200rpx;
      border: 2rpx dashed #c0c4cc;
      border-radius: 12rpx;
      background: #fafafa;
      transition: all 0.3s;
      &:active {
        background: #f0f2f5;
        border-color: #007aff;
      }
      .upload-hint {
        font-size: 28rpx;
        color: #606266;
        margin-top: 16rpx;
        margin-bottom: 8rpx;
      }
      .upload-tip {
        font-size: 24rpx;
        color: #909399;
      }
    }
  }
  .signature-disabled {
    .signature-disabled-area {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 200rpx;
      border: 2rpx solid #e4e7ed;
      border-radius: 12rpx;
      background: #f8f9fa;
      .disabled-hint {
        font-size: 28rpx;
        color: #909399;
        margin-top: 16rpx;
        margin-bottom: 8rpx;
      }
      .disabled-tip {
        font-size: 24rpx;
        color: #c0c4cc;
      }
    }
  }
}
// 签名模态框样式
.signature-modal {
  padding: 30rpx;
  background: #fff;
  border-radius: 20rpx 20rpx 0 0;
  max-height: 80vh;
  overflow: hidden;
  .modal-header {
    text-align: center;
    margin-bottom: 30rpx;
    .modal-title {
      font-size: 32rpx;
      font-weight: 600;
      color: #303133;
      display: block;
      margin-bottom: 8rpx;
    }
    .modal-subtitle {
      font-size: 24rpx;
      color: #909399;
    }
  }
  .signature-canvas-container {
    .signature-canvas {
      display: block;
      margin: 0 auto;
      border: 2rpx solid #e4e7ed;
      border-radius: 8rpx;
      background: #fff;
      touch-action: none;
    }
    .canvas-actions {
      display: flex;
      gap: 20rpx;
      margin: 20rpx 0;
      padding: 0 20rpx;
      .action-btn {
        flex: 1;
        height: 60rpx;
        border-radius: 30rpx;
        border: 1rpx solid #dcdfe6;
        background: #fff;
        font-size: 24rpx;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 8rpx;
        &.clear-btn {
          color: #f56c6c;
          border-color: #f56c6c;
        }
        &.redo-btn {
          color: #1890ff;
          border-color: #1890ff;
          &:disabled {
            color: #c0c4cc;
            border-color: #e4e7ed;
          }
        }
        &.confirm-btn {
          background: linear-gradient(135deg, #0f95b0, #89c4c1);
          color: #fff;
          border: none;
          &:disabled {
            background: #c0c4cc;
            opacity: 0.6;
          }
        }
        &:active:not(:disabled) {
          transform: scale(0.98);
        }
      }
    }
    .signature-preview {
      margin-top: 20rpx;
      padding: 20rpx;
      background: #f8f9fa;
      border-radius: 8rpx;
      .preview-title {
        display: block;
        font-size: 24rpx;
        color: #606266;
        margin-bottom: 12rpx;
      }
      .preview-image {
        width: 200rpx;
        height: 80rpx;
        border: 1rpx solid #dcdfe6;
        border-radius: 4rpx;
        background: #fff;
      }
    }
  }
@@ -565,5 +1811,26 @@
      }
    }
  }
  .signature-modal {
    padding: 20rpx;
    .signature-canvas-container {
      .signature-canvas {
        width: 100% !important;
        height: 250rpx !important;
      }
      .canvas-actions {
        // flex-direction: column;
        gap: 12rpx;
        padding: 0;
        .action-btn {
          height: 72rpx;
        }
      }
    }
  }
}
</style>