<script setup>
import TopNav from "@/components/topNav.vue";
import {
  onMounted,
  reactive,
  ref,
  computed,
  nextTick,
  onUnmounted,
  watch,
} from "vue";
import { useRoute } from "vue-router";
import Timeline from "@/pages/clockInPage/components/Timeline.vue";
import {
  fetchPatrolList,
  fetchSelectByBddxlrwId,
  fetchSelectListByBddxlrwId,
  fetchTbZdxlFgdwBddxlrwJlClockIn,
} from "@/api/patrolList";
import { getBase64, hintToast } from "@/utils/tools";
import { ImagePreview } from "vant";
import { qcckPost, qcckGet } from "@/api/qcckApi";
import MapWrapper from "@/pages/clockInPage/components/mapWrapper.vue";
import emitter from "@/utils/eventBus";

const route = useRoute();
const active = ref(0);
const nextStep = ref(0);
const baseUrl = ref("");
const fileId = ref("");
const startTime = ref("2025-09-18 18:15:00");
const useCountdownFromTime = (minutes = 10) => {
  const timeLeft = ref(0); // 剩余毫秒数
  const timer = ref(null);
  const isRunning = ref(false); // 是否运行中
  const isExpired = ref(false); // 是否过期
  const expirationTime = ref(null); // 过期时间

  // 计算过期时间
  const calculateExpirationTime = (time) => {
    if (!time) return null;

    try {
      const startDate = new Date(time);
      if (isNaN(startDate.getTime())) return null;

      return new Date(startDate.getTime() + minutes * 60000);
    } catch {
      return null;
    }
  };

  // 更新倒计时
  const update = () => {
    if (!expirationTime.value) {
      isExpired.value = true;
      isRunning.value = false;
      return;
    }

    const now = new Date();
    timeLeft.value = expirationTime.value - now;

    if (timeLeft.value <= 0) {
      timeLeft.value = 0;
      isExpired.value = true;
      isRunning.value = false;
      stop();
      return;
    }

    if (timeLeft.value > 0) {
      timer.value = setTimeout(update, 1000);
    }
  };

  // 开始倒计时
  const start = (startTime) => {
    stop();
    expirationTime.value = calculateExpirationTime(startTime);

    if (!expirationTime.value) {
      isExpired.value = true;
      return;
    }

    isRunning.value = true;
    isExpired.value = false;
    update();
  };

  // 停止倒计时
  const stop = () => {
    if (timer.value) {
      clearTimeout(timer.value);
      timer.value = null;
    }
    isRunning.value = false;
  };

  // 格式化时间显示
  const formattedTime = computed(() => {
    if (timeLeft.value <= 0) return "00:00";

    const totalSeconds = Math.floor(timeLeft.value / 1000);
    const mins = Math.floor(totalSeconds / 60);
    const secs = totalSeconds % 60;

    return `${mins.toString().padStart(2, "0")}:${secs
      .toString()
      .padStart(2, "0")}`;
  });

  // 过期时间显示
  const expirationTimeFormatted = computed(() => {
    if (!expirationTime.value) return "";
    return expirationTime.value.toLocaleTimeString("zh-CN", {
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
      hour12: false,
    });
  });

  // 自动清理
  onUnmounted(stop);

  return {
    timeLeft,
    formattedTime,
    expirationTime: expirationTimeFormatted,
    isRunning,
    isExpired,
    start,
    stop,
  };
};

const data = reactive({
  patroObj: {},
  info: [],
  query: {},
});

const infoData = computed(() => {
  return data.info?.[nextStep.value];
});
const activeInfoData = computed(() => data.patroObj?.bddList?.[active.value]);
const { formattedTime, isExpired, expirationTime, start, stop } =
  useCountdownFromTime(10);
// 删除打卡图片
const clearImage = () => {
  baseUrl.value = "";
  fileId.value = "";
};

// 浏览图片
const onClickImg = (url) => {
  ImagePreview([url]);
};

// 点击上传
const photoFn = () => {
  try {
    bridge.pZ("photo");
  } catch (err) {
    console.log(err, "err");
  }
};

// 支持更大数字的转换
const getChineseNumber = (num) => {
  const numbers = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];

  if (num <= 10) {
    return numbers[num - 1];
  } else if (num <= 19) {
    return `十${numbers[num - 11] || ""}`;
  } else if (num <= 99) {
    const tens = Math.floor(num / 10);
    const units = num % 10;
    return `${numbers[tens - 1]}十${units > 0 ? numbers[units - 1] : ""}`;
  } else {
    return num.toString(); // 超过99返回阿拉伯数字
  }
};
// 切换tab
const onChange = (value) => {
  active.value = value;
  nextStep.value = 0;
  baseUrl.value = "";
  fileId.value = "";
  getPatrolList(data?.query);
};

const handleNext = (index) => {
  stop();
  nextStep.value = index;
  start(infoData?.value?.dkKsSj);
};

function setimage_base64(pzid, base64) {
  console.log(base64, "base64");
  baseUrl.value = `data:image/jpeg;base64,${base64}`;
  qcckPost({ base64: base64 }, "/mosty-base/minio/image/upload/base64").then(
    (res) => {
      fileId.value = res;
    }
  );
}

const imageCache = new Map();

const getImageUrl = async (fileId) => {
  if (!fileId) return null;
  // 检查缓存
  if (imageCache.has(fileId)) {
    return imageCache.get(fileId);
  }
  try {
    const res = await qcckGet({}, `/mosty-base/minio/file/download/${fileId}`);
    if (res?.url) {
      const base64 = await getBase64("", res.url);
      // 存入缓存
      imageCache.set(fileId, base64);
      return base64;
    }
    return null;
  } catch (error) {
    console.warn("获取图片失败:", error);
    return null;
  }
};

const getData = async () => {
  try {
    const res = await fetchSelectListByBddxlrwId({
      bddxlrwId: activeInfoData?.value?.id || "",
    });
    if (res && res?.length > 0) {
      // 获取所有唯一的图片ID
      const uniqueImageIds = new Set();
      res?.forEach((item) => {
        if (item?.dkJsFj) uniqueImageIds.add(item.dkJsFj);
        if (item?.dkKsFj) uniqueImageIds.add(item.dkKsFj);
      });
      // 批量获取图片URL
      const imageEntries = await Promise.allSettled(
        Array.from(uniqueImageIds).map(async (id) => {
          try {
            const url = await getImageUrl(id);
            return { id, url };
          } catch (error) {
            return { id, url: null };
          }
        })
      );

      // 创建图片映射表
      const imageMap = new Map();
      imageEntries.forEach((entry) => {
        if (entry.status === "fulfilled") {
          imageMap.set(entry.value.id, entry.value.url);
        }
      });

      // 设置数据
      data.info = res.map((item, index) => ({
        ...item,
        count: getChineseNumber(index + 1),
        imgUrlDkJsFj: item?.dkJsFj ? imageMap.get(item.dkJsFj) : null,
        imgUrlDkKsFj: item?.dkKsFj ? imageMap.get(item.dkKsFj) : null,
      }));

      // 打卡结束经纬度
      const dkJs = res?.map((i) => ({ jd: i?.dkJsJd, wd: i?.dkJsWd }));
      // 打卡开始经纬度
      const dkKs = res?.map((i) => ({ jd: i?.dkKsJd, wd: i?.dkKsWd }));

      // 删除标注
      emitter.emit("deletePointArea", "annotationDkKs");
      //地图撒点然后移动
      emitter.emit("addPointArea", {
        coords: [...dkJs, ...dkKs],
        icon: require("../../assets/images/11.png"),
        flag: "annotationDkKs",
      });

      await nextTick(() => {
        const firstItem = data.info[nextStep.value || 0];
        if (firstItem?.dkKsSj) {
          startTime.value = infoData?.value?.dkKsSj;
          start(startTime.value);
        } else {
          start("");
        }
      });
    }
    if (activeInfoData.value) {
      const result = await fetchSelectByBddxlrwId(
        activeInfoData?.value?.id || ""
      );
      if (result) {
        const { jd, wd } = result;
        // 删除标注
        emitter.emit("deletePointArea", "checkPoint");
        //地图撒点然后移动
        emitter.emit("addPointArea", {
          coords: [{ jd, wd }],
          icon: require("../../assets/lz/peoplePolice.png"),
          flag: "checkPoint",
        });
      }
    }
  } catch (error) {
    console.error(error);
  }
};

//获取当前位置信息
function getUserLocation(sfdw) {
  let { lng, lat } = getLocation();
  if (lng && lat) {
    emitter.emit("deletePointArea", "dw");
    //地图撒点然后移动
    emitter.emit("addPointArea", {
      coords: [{ jd: lng, wd: lat }],
      icon: require("../../assets/lz/dw.png"),
      flag: "dw",
      sfdw: sfdw,
      sizeX: 30,
      sizeY: 35,
    });
  } else {
    hintToast("暂无坐标信息");
  }
}

const getPatrolList = async ({ current, id }) => {
  const res = await fetchPatrolList({ pageCurrent: current, pageSize: 10 });
  if (res?.records.length > 0) {
    getUserLocation();
    data.patroObj = res?.records?.find((item) => item.id === id) || {};
    // 删除方格
    emitter.emit("deletePointArea", "zdxl_fzyc");
    // 生成方格
    const { x1, y1, x2, y2, fgId, zxX, zxY, fgMc } = data.patroObj;
    const centerPoint = [zxX, zxY];
    const position = [
      [Number(x1), Number(y1)],
      [Number(x2), Number(y2)],
    ];
    const text = fgMc;
    const obj = [
      { position: position, text, id: fgId, userData: activeInfoData?.value },
    ];
    emitter.emit("echoPlane", {
      fontColor: "#12fdb8",
      coords: obj,
      type: "rectangle",
      flag: "zdxl_fzyc",
      color: "rgba(2,20,51,0.5)",
      linecolor: "#1C97FF",
    });
    emitter.emit("setMapCenter", { location: centerPoint, zoomLevel: 12 });
    await getData();
  }
};

// 简单的时间加法函数
const addTenMinutes = (timeString) => {
  if (!timeString) return "";

  const date = new Date(timeString);
  if (isNaN(date.getTime())) return "";

  date.setMinutes(date.getMinutes() + 10);

  return date
    .toLocaleString("zh-CN", {
      year: "numeric",
      month: "2-digit",
      day: "2-digit",
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
      hour12: false,
    })
    .replace(/\//g, "-");
};

// 判断是否超过10分钟
const isTenMinutesPassed = (startTime) => {
  if (!startTime) return false;

  try {
    const startDate = new Date(startTime);
    if (isNaN(startDate.getTime())) return false;

    // 计算10分钟后的时间
    const tenMinutesLater = new Date(startDate.getTime() + 10 * 60 * 1000);
    const now = new Date();

    // 判断当前时间是否超过10分钟后
    return now > tenMinutesLater;
  } catch (error) {
    console.error("时间判断错误:", error);
    return false;
  }
};

// 打卡
const handleClick = async () => {
  if (data.info.length == 0) {
    hintToast("此方格没有必打卡点位！");
    return;
  }
  const { id } = data.info?.[nextStep.value];

  if (!isTenMinutesPassed(infoData?.value.dkKsSj) && infoData?.value.dkKsSj) {
    const newTime = addTenMinutes(infoData?.value.dkKsSj);
    hintToast(`请于${newTime.split(" ")[1]}后打卡`);
    return;
  }

  if (fileId.value === "") {
    hintToast("请拍照再打卡");
    return;
  }

  const { lng, lat } = getLocation();
  try {
    const res = await fetchTbZdxlFgdwBddxlrwJlClockIn({
      id,
      dkWd: lat,
      dkJd: lng,
      dkFj: fileId.value,
    });

    if (res) {
      hintToast(`打卡成功`);
      baseUrl.value = ""; //打卡成功需要把图片删除掉
      fileId.value = "";
      await getData();
      start();
    }
  } catch (error) {
    hintToast(`打卡异常`);
    stop();
    console.log(error);
  }
};

onMounted(() => {
  if (route?.query) {
    data.query = route?.query;
    getPatrolList(route?.query);
  }

  window.setimagebase64 = setimage_base64;
});
</script>

<template>
  <div>
    <top-nav navTitle="打卡" :showLeft="true" />
    <div class="clockInWrapper">
      <van-tabs v-model:active="active" @click="onChange">
        <template v-for="(item, index) in data.patroObj?.bddList" :key="index">
          <van-tab :title="item.bddMc" />
        </template>
      </van-tabs>

      <div class="clockInList">
        <template v-for="(item, index) in data.info" :key="index">
          <div
            :class="['clockInList_item', { active: nextStep === index }]"
            @click="handleNext(index)"
          >
            <div class="label">{{ `第${item?.count}次打卡` }}</div>
            <div class="dec">
              <van-icon v-if="item?.dkKsSj" name="checked" color="#007DE9" />
              <div>开始</div>
              <div v-if="item?.dkKsSj" class="time">
                {{ item?.dkKsSj?.split(" ")[1] }}已打卡
              </div>
            </div>
            <div class="dec">
              <van-icon v-if="item?.dkJsSj" name="checked" color="#007DE9" />
              <div>离开</div>
              <div v-if="item?.dkJsSj" class="time">
                {{ item?.dkJsSj?.split(" ")[1] }}已打卡
              </div>
            </div>
          </div>
        </template>
      </div>

      <div class="upload_box">
        <div class="image_box" v-if="baseUrl">
          <van-icon
            name="close"
            class="close_icon"
            @click="clearImage"
            color="#000"
            size="24px"
          />
          <van-image
            :src="baseUrl"
            @click="onClickImg(baseUrl)"
            style="flex: 1"
          >
            <template v-slot:loading>
              <van-loading type="spinner" size="20" />
            </template>
          </van-image>
        </div>
        <div class="upload_icon_box" v-else>
          <van-icon @click="photoFn" color="#1DB1FF" name="plus" />
          <span>点击拍照</span>
        </div>
        <!-- <van-uploader v-model="clockList" :max-count="1" :after-read="afterRead" capture="camera"
          :before-read="beforeRead" accept="image/*" /> -->
        <div class="upload_tip">
          <span style="color: red">*</span>须拍摄实景图才可进行打卡
        </div>
      </div>

      <div class="clockWrapper">
        <div
          v-if="!infoData?.dkJsSj || !infoData?.dkKsSj"
          class="circleWrapper"
          :class="{
            disabled: (!isExpired && expirationTime) || infoData?.dkJsSj,
          }"
          @click="handleClick"
        >
          <div v-if="!isExpired && expirationTime" class="time">
            {{ formattedTime }}后
          </div>
          <div class="title">{{ !infoData?.dkKsSj ? `开始` : `离开` }}</div>
          <div class="info">{{ `第${infoData?.count || ""}次打卡` }}</div>
        </div>
        <div
          v-else
          class="circleWrapper"
          :class="{
            disabled: (!isExpired && expirationTime) || infoData?.dkJsSj,
          }"
        >
          <div class="title">已结束</div>
          <!--          <div class="info">{{ `第${infoData?.count || ''}次打卡` }}</div>-->
        </div>
        <div class="circleWrapperTip">
          <van-icon name="success" color="#FFFFFF" />
          <div>已进入考勤范围：{{ activeInfoData?.bddMc }}</div>
        </div>
      </div>

      <timeline v-if="data.info.length > 0" :data="[data.info?.[nextStep]]" />
      <map-wrapper />
    </div>
  </div>
</template>

<style scoped lang="scss">
.clockInWrapper {
  margin-top: 13vw;
  padding: 2vw;

  .clockWrapper {
    margin-top: 23.47vw;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

    .circleWrapperTip {
      display: flex;
      align-items: center;
      justify-content: center;
      margin-top: 7.2vw;
      font-family: PingFang HK, PingFang HK;
      font-weight: 400;
      font-size: 3.73vw;
      color: #707070;

      ::v-deep {
        .van-icon-success {
          margin-right: 1.33vw;
          display: flex;
          align-items: center;
          justify-content: center;
          width: 3.73vw;
          height: 3.73vw;
          background: #11aa66;
          border-radius: 13.33vw;
        }
      }
    }

    .circleWrapper {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      width: 42.67vw;
      height: 42.67vw;
      background: linear-gradient(180deg, #1db1ff 0%, #007de9 100%);
      border-radius: 26.67vw;
      color: #fff;
      font-family: PingFang HK, PingFang HK;

      .time {
        font-weight: 400;
        font-size: 4.8vw;
        margin-bottom: 1.33vw;
      }

      .title {
        font-weight: 400;
        font-size: 4.8vw;
      }

      .info {
        font-weight: 400;
        font-size: 3.73vw;
        margin-top: 1.33vw;
      }
    }

    .disabled {
      background: #ededed !important;
      color: #75787f !important;
    }
  }

  .upload_box {
    margin-top: 4vw;
    display: flex;
    padding-bottom: 4vw;
    border-bottom: 0.27vw solid #d9d9d9;

    .upload_tip {
      color: #999999;
      font-size: 2.67vw;
      margin-left: 2.67vw;
    }

    .image_box {
      position: relative;
      width: 40vw;
      height: 24vw;

      .close_icon {
        position: absolute;
        right: 0;
        top: 0;
        z-index: 20;
      }

      ::v-deep .van-image {
        width: 100%;
      }
    }

    .upload_icon_box {
      font-size: 3.2vw;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      color: #1db1ff;
      border: 0.27vw dashed #1db1ff;
      text-align: center;
      width: 29.33vw;
      height: 16.53vw;
      border-radius: 2.67vw;
    }
  }

  .clockInList {
    display: flex;
    justify-content: space-between;
    margin-top: 4vw;
    overflow-y: auto;

    .clockInList_item {
      padding: 4vw 2vw;
      width: 43vw;
      height: 18vw;
      background: #ededed;
      border-radius: 2.67vw;
      flex-shrink: 0;
      margin-right: 2.67vw;

      &:last-child {
        margin-right: 0;
      }

      .label {
        font-weight: 400;
        font-size: 4.27vw;
      }

      .dec {
        display: flex;
        align-items: center;
        color: #75787f;
        font-size: 3.73vw;
        margin-top: 1.33vw;

        .van-icon-checked {
          margin-right: 1.33vw;
        }
        .time {
          margin-left: 1.33vw;
        }
      }
    }

    .active {
      background: rgba(62, 110, 232, 0.2);
    }
  }

  ::v-deep {
    .van-tabs__nav {
      flex-wrap: wrap;
    }
    .van-tabs__nav--line {
      padding-bottom: 0 !important;
    }

    .van-tabs__wrap {
      height: auto !important;
    }

    .van-tabs__line {
      opacity: 0;
    }

    .van-tab {
      flex: initial;
      font-size: 4vw;
      width: auto !important;
      height: 9.33vw;
      padding: 0 2.67vw;
      border: 0.27vw solid #ededed;
      color: #75787f;
      flex-shrink: 0;
      border-radius: 2vw;
      margin-right: 2vw;
      margin-bottom: 4px;

      &:last-child {
        margin-right: 0 !important;
      }
    }

    .van-tab--active {
      background: rgba(62, 110, 232, 0.2);
      border: 0.27vw solid #3e6ee8;
      color: #3e6ee8 !important;
    }
  }
}
</style>
Powered by Gitea
当前版本: 1.24.2 页面:
176ms
模板:
102ms
许可证
API