修改打卡间隔时间

This commit is contained in:
13684185576
2025-12-04 11:06:30 +08:00
parent 52d4a65473
commit fb02dd45d1
2 changed files with 776 additions and 1 deletions

View File

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

View File

@ -0,0 +1,775 @@
<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