Files
dy_app/src/pages/clockInPage/indexbf.vue
2025-12-04 11:06:30 +08:00

775 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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