新增必到点采集详情页面 处理打卡

This commit is contained in:
maojiacai
2025-09-08 19:33:18 +08:00
parent 8aad9f302f
commit e4767754d5
11 changed files with 563 additions and 121 deletions

View File

@ -1,32 +1,45 @@
<template>
<div class="info-container">
<div class="item" v-for="(item, index) in data" :key="index">
<div class="point"></div>
<div class="line" v-if="index + 1 != data.length"></div>
<div class="info-right">
<div class="name">{{item.name}}</div>
<div class="time">打卡时间<text>{{ item.time }}</text></div>
<template v-for="(item, index) in data" :key="index">
<div v-if="item?.dkKsSj" class="item">
<div class="point"></div>
<div class="line" v-if="index + 1 != data.length"></div>
<div class="info-right">
<div class="name">{{ `${item?.count}次打卡` }}</div>
<div class="time">打卡时间<text>{{ item?.dkKsSj }}</text></div>
<div class="image">
<img src="../../../assets/home/bddcj.png" alt="">
</div>
<div v-if="item?.imgUrl" class="image">
<van-image width="80px" :src="item?.imgUrl" @click="onClickImg(item?.imgUrl)" style="flex: 1">
<template v-slot:loading>
<van-loading type="spinner" size="20" />
</template>
</van-image>
</div>
<div class="address">
<van-icon name="location-o" color="#1DB1FF" />
<div class="name">四川省成都市</div>
<!-- <div class="address">-->
<!-- <van-icon name="location-o" color="#1DB1FF" />-->
<!-- <div class="name">四川省成都市</div>-->
<!-- </div>-->
</div>
</div>
</div>
</template>
</div>
</template>
<script setup>
import {ImagePreview} from "vant";
const props = defineProps({
data: {
type: Array,
default: []
}
})
//预览图片
function onClickImg(url) {
ImagePreview([url]);
}
</script>
<style lang="scss" scoped>

View File

@ -1,43 +1,158 @@
<script setup>
import TopNav from "@/components/topNav.vue";
import {onMounted, reactive, ref} from "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 {fetchSelectListByBddxlrwId} from "@/api/patrolList";
import {fetchSelectListByBddxlrwId, fetchTbZdxlFgdwBddxlrwJlClockIn} from "@/api/patrolList";
import {getBase64, hintToast} from "@/utils/tools";
import {ImagePreview} from "vant";
import {qcckPost, qcckGet} from "@/api/qcckApi";
const route = useRoute();
const activeName = ref("0")
const active = ref(0)
const nextStep = ref(0)
const baseUrl = ref("")
const fileId = ref("")
const startTime = ref("2025-09-08 15:29: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({
tabsList: [],
info: []
info: [],
})
const timeList = [
{
time:'09:24:34',
name: '第一次打卡 开始',
person:'李小明',
content:'签收订单, 订单状态变更为待回单, 签收备注为\'无\'',
},
{
time:'2020-03-26 12:30:12',
name: '吾悦广场',
person:'李小明',
content:'创建了订单, 并添加了跟踪方式, 电子设备或快递单号: 854654875',
},
{
time:'2020-03-26 12:30:12',
name: '吾悦广场',
person:'李小明',
content:'签收订单, 订单状态变更为待回单, 签收备注为\'无\'',
const infoData = computed(() => {
return data.info?.[nextStep.value]
})
const activeInfoData = computed(() => data.tabsList?.[active.value])
const { formattedTime, isExpired, expirationTime, start, stop } = useCountdownFromTime(10);
// 删除打卡图片
const clearImage = () => {
baseUrl.value = ""
}
// 浏览图片
const onClickImg = (url) => {
ImagePreview([url])
}
// 点击上传
const photoFn = () => {
try {
bridge.pZ("photo");
} catch (err) {
console.log(err, 'err');
}
]
const clearImage = () => {}
const onClickImg = () => {}
const photoFn = () => {}
}
const count = (item) => {
if (!item || !item.dkSx) return undefined;
@ -48,20 +163,178 @@ const count = (item) => {
return numbers[index];
};
const onChange = (value) => {
active.value = value;
getData(data.tabsList[value]?.id)
}
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 (bddxlrwId = '') => {
const res = await fetchSelectListByBddxlrwId({ bddxlrwId })
if (res) {
data.info = res
try {
const res = await fetchSelectListByBddxlrwId({ bddxlrwId });
if (res) {
// 收集所有需要加载的图片ID
const imageIds = res.map(i => i?.dkJsFj).filter(Boolean);
// 批量获取图片URL
const imagePromises = imageIds.map(id => getImageUrl(id));
const imageResults = await Promise.allSettled(imagePromises);
// 创建图片映射表
const imageMap = new Map();
imageIds.forEach((id, index) => {
const result = imageResults[index];
imageMap.set(id, result.status === 'fulfilled' ? result.value : null);
});
// 设置数据
data.info = res.map(i => ({
...i,
count: count(i),
imgUrl: imageMap.get(i?.dkJsFj) || null
}));
active.value = 1;
console.log('完整数据:', data.info);
await nextTick(() => {
const firstItem = data.info[nextStep.value || 0];
if (firstItem?.dkKsSj) {
startTime.value = infoData?.value?.dkKsSj;
start(startTime.value);
} else {
start('');
}
});
}
} catch (error) {
console.error(error);
}
};
// 简单的时间加法函数
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 () => {
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(`打卡成功`)
await getData(data.tabsList?.[0]?.id)
start();
}
} catch (error) {
hintToast(`打卡异常`)
stop()
console.log(error);
}
}
onMounted(() => {
if (route.query.item) {
data.tabsList = JSON.parse(route.query.item)
console.log(data.tabsList)
}
getData(data.tabsList[0]?.id)
getData(data.tabsList?.[0]?.id)
window.setimagebase64 = setimage_base64;
})
</script>
@ -70,30 +343,28 @@ onMounted(() => {
<top-nav navTitle="打卡" :showLeft="true" />
<div class="clockInWrapper">
<van-tabs v-model="activeName">
<template v-for="(item, index) in data.tabsList" :key="item?.id">
<van-tab :name="index" :title="item?.bddMc" />
<van-tabs v-model:active="active" :active="active" @click="onChange">
<template v-for="(item, index) in data.tabsList" :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">
<div class="label">{{ `${count(item)}次打卡` }}</div>
<div class="dec">开始</div>
<div class="dec">离开</div>
<div class="clockInList_item" @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 class="clockInList_item">-->
<!-- <div class="label">第一次打卡</div>-->
<!-- <div class="dec">开始</div>-->
<!-- <div class="dec">离开</div>-->
<!-- </div>-->
<!-- <div class="clockInList_item">-->
<!-- <div class="label">第一次打卡</div>-->
<!-- <div class="dec">开始</div>-->
<!-- <div class="dec">离开</div>-->
<!-- </div>-->
</div>
<div class="upload_box">
@ -115,18 +386,18 @@ onMounted(() => {
</div>
<div class="clockWrapper">
<div class="circleWrapper">
<div class="time">100000</div>
<div class="title">开始</div>
<div class="info">第一次打卡</div>
<div class="circleWrapper" :class="{ 'disabled': !isExpired && expirationTime }" @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 class="circleWrapperTip">
<van-icon name="success" color="#FFFFFF" />
<div>已进入考勤范围打卡点1德阳市某某某</div>
<div>已进入考勤范围{{ activeInfoData?.bddMc }}</div>
</div>
</div>
<timeline :data="timeList" />
<timeline :data="data.info" />
</div>
</div>
</template>
@ -182,6 +453,7 @@ onMounted(() => {
.time {
font-weight: 400;
font-size: 4.8vw;
margin-bottom: 1.33vw;
}
.title {
@ -192,8 +464,14 @@ onMounted(() => {
.info {
font-weight: 400;
font-size: 3.73vw;
margin-top: 1.33vw;
}
}
.disabled {
background: #EDEDED !important;
color: #75787F !important;
}
}
.upload_box {
@ -244,6 +522,7 @@ onMounted(() => {
display: flex;
justify-content: space-between;
margin-top: 4vw;
overflow-y: auto;
.clockInList_item {
padding: 4vw 2vw;
@ -251,6 +530,12 @@ onMounted(() => {
height: 18vw;
background: #EDEDED;
border-radius: 2.67vw;
flex-shrink: 0;
margin-right: 2.67vw;
&:last-child {
margin-right: 0;
}
.label {
font-weight: 400;
@ -258,9 +543,18 @@ onMounted(() => {
}
.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;
}
}
}
}
@ -275,7 +569,7 @@ onMounted(() => {
}
.van-tabs__line {
display: none;
opacity: 0;
}
.van-tab {