clockIn.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. <template>
  2. <view class="container">
  3. <!-- 时间日期信息 -->
  4. <view class="header">
  5. <text class="day">{{ currentDay }}</text>
  6. <text class="date">{{ currentDate }}</text>
  7. <text class="nowTime">{{ nowTime }}</text>
  8. </view>
  9. <!-- 打卡记录 -->
  10. <view class="record">
  11. <view class="record-item" v-if="nowTime <= '12:00:00' || signName == '上班签到'">
  12. <text class="ygoa-icon icon-goToWork"></text>
  13. <text class="title">上班</text>
  14. <text class="time">{{ signInTime || '未打卡' }}</text>
  15. <text class="signStatus" v-if="!isSignInStatusDisabled">({{ signInStatusName }})</text>
  16. </view>
  17. <view class="record-item" v-else-if="nowTime > '12:00:00' && signName == '下班签退'">
  18. <text class="ygoa-icon icon-afterWork"></text>
  19. <text class="title">下班</text>
  20. <text class="time">{{ signOutTime || '未打卡' }}</text>
  21. <text class="signStatus" v-if="!isSignOutStatusDisabled" style="color: #e64340;">({{ signOutStatusName
  22. }})</text>
  23. </view>
  24. <view class="record-item" @click="getAddress">
  25. <text class="ygoa-icon icon-location"></text>
  26. <text class="title">当前位置</text>
  27. <text class="time">{{ address || '点击获取定位' }}</text>
  28. </view>
  29. </view>
  30. <!-- 地图 -->
  31. <atl-map :mapKey="config.mapKey" :mapType="mapType" :longitude="119.213036" :latitude="25.958812"
  32. :marker="marker" :polygons="polygons" :isPolygons="true">
  33. <template v-slot:content>
  34. </template>
  35. </atl-map>
  36. <!-- 打卡按钮 -->
  37. <view class="footer">
  38. <button type="default" @click="signInOrOut" style="background-color: #3c9cff; color: #fcfcfc;"
  39. v-if="signName == '上班签到'">上班签到</button>
  40. <button type="default" @click="signInOrOut" style="background-color: #1aad19; color: #fcfcfc;"
  41. v-else-if="signName == '下班签退'">下班签退</button>
  42. </view>
  43. </view>
  44. </template>
  45. <script setup lang="ts">
  46. import {onBeforeUnmount,onMounted,reactive,ref} from 'vue';
  47. // 导入地理位置相关函数
  48. import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
  49. import {point,polygon} from "@turf/helpers";
  50. import {useUserStore} from '@/store/user';
  51. import $modal from '@/plugins/modal.js'
  52. import $tab from '@/plugins/tab.js';
  53. import config from '@/config.js';
  54. import {createAttendance,tranAddress,checkAttendance} from '@/api/mine.js'
  55. const userStore = useUserStore();
  56. const thisUser = userStore.user;
  57. const intervalId = ref(null); // 定时器ID
  58. onBeforeUnmount(() => {
  59. clearInterval(intervalId.value);
  60. })
  61. // 组件挂载后初始化当前日期与位置
  62. onMounted(() => {
  63. dateInit();
  64. // getlocation();
  65. });
  66. function showNowTime() {
  67. // let nowMilliseconds=Date.now();
  68. // intervalId.value = setInterval(() => {
  69. // nowMilliseconds+=1000;
  70. // nowTime.value = new Date(nowMilliseconds).toLocaleString('zh-CN', {
  71. // year: 'numeric',
  72. // month: '2-digit',
  73. // day: '2-digit',
  74. // hour: '2-digit',
  75. // minute: '2-digit',
  76. // second: '2-digit',
  77. // hour12: false // 24小时制
  78. // });
  79. // }, 1000); // 每1秒更新一次
  80. intervalId.value = setInterval(() => {
  81. nowTime.value = getNowTime()
  82. }, 1000); // 每1秒更新一次
  83. }
  84. // 当前日期和时间相关信息
  85. const currentDate = ref('2023-04-01');
  86. const currentDay = ref('星期三');
  87. const nowTime = ref(''); // 当前时间
  88. const weekArr = reactive(['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']); // 星期数组
  89. // 初始化日期
  90. function dateInit() {
  91. const nowDate = new Date();
  92. const year = nowDate.getFullYear(); // 获取年份
  93. const month = nowDate.getMonth() + 1; // 获取月份
  94. const day = nowDate.getDate(); // 获取日期
  95. // 格式化月份和日期为两位数
  96. const formattedMonth = month < 10 ? '0' + month : month;
  97. const formattedDay = day < 10 ? '0' + day : day;
  98. currentDate.value = `${year}-${formattedMonth}-${formattedDay}`; // 设置当前日期
  99. const dayOfWeek = nowDate.getDay(); // 获取当前星期
  100. currentDay.value = weekArr[dayOfWeek]; // 设置当前星期
  101. showNowTime();
  102. getTodayAtt();
  103. }
  104. const signInTime = ref(''); // 上班签到时间
  105. const signOutTime = ref(''); // 下班签到时间
  106. //打卡按钮文字显示
  107. const signName = ref('')
  108. //迟到早退状态,默认隐藏
  109. const signInStatusName = ref('迟到')
  110. const signOutStatusName = ref('早退')
  111. const isSignInStatusDisabled = ref(true)
  112. const isSignOutStatusDisabled = ref(true)
  113. //获取今天考勤状态
  114. function getTodayAtt() {
  115. const params = {
  116. universalid: thisUser.useId,
  117. rizi: currentDate.value
  118. }
  119. checkAttendance(params).then(res => {
  120. if ("success" == res.returnMsg) {
  121. // console.log("kaoqin",res);
  122. const attList = res.returnParams.list;
  123. const time1 = attList.find(item => item.att_type_id === '1');
  124. //拿到所有签退数据后,用pop取最后一个
  125. const time2 = attList.filter(item => item.att_type_id === '2').pop();
  126. console.log('time2', time2);
  127. if (time1 !== undefined) {
  128. // console.log('getTime1',time1);
  129. signInTime.value = time1.att_time.split(' ')[1];
  130. signName.value = '下班签退';
  131. if (Array.isArray(config.lateTimeRange) && isTimeInRange(signInTime.value, config
  132. .lateTimeRange[0], '23:59:59')) {
  133. isSignInStatusDisabled.value = false; //迟到
  134. }
  135. } else {
  136. signName.value = '上班签到';
  137. }
  138. if (time2 !== undefined) {
  139. signOutTime.value = time2.att_time.split(' ')[1];
  140. if (Array.isArray(config.signOutTimeRange) && isTimeInRange(signOutTime.value, "00:00:00",
  141. config.signOutTimeRange[0])) {
  142. isSignOutStatusDisabled.value = false; //早退
  143. } else {
  144. isSignOutStatusDisabled.value = true; //隐藏早退状态
  145. }
  146. }
  147. }
  148. })
  149. }
  150. //上班卡或下班卡
  151. function signInOrOut() {
  152. if ('上班签到' == signName.value) {
  153. signIn();
  154. } else {
  155. signOut();
  156. }
  157. }
  158. //上班签到
  159. function signIn() {
  160. const now = getNowTime();
  161. //判断打卡时间是否符合
  162. if (!isTimeInRange(now, ...config.signInTimeRange)) {
  163. $modal.msg('不在签到时间8:30--9:30内,请前往补卡')
  164. return;
  165. }
  166. // attType.value=1;
  167. const signIn = 1;
  168. getAddress().then(() => {
  169. clockIn(signIn);
  170. });
  171. };
  172. // 下班签退
  173. function signOut() {
  174. const now = getNowTime();
  175. const signOut = 2;
  176. //判断打卡时间是否符合
  177. if (!isTimeInRange(now, ...config.signOutTimeRange)) {
  178. $modal.confirm('当前非正常签退时间,是否继续?').then(res => {
  179. if (res) {
  180. getAddress().then(() => {
  181. clockIn(signOut); // 执行打卡并获取结果
  182. });
  183. }
  184. }).catch(() => { })
  185. } else {
  186. getAddress().then(() => {
  187. clockIn(signOut);
  188. });
  189. }
  190. };
  191. //获取当前时间
  192. function getNowTime() {
  193. return new Date().toLocaleTimeString('en-US', {
  194. hour12: false
  195. }).substring(0, 8)
  196. }
  197. //判断时间是否在区间内
  198. function isTimeInRange(time, startTime, endTime) {
  199. // 将时间字符串转换为Date对象
  200. const timeObj = new Date('1970-01-01T' + time + 'Z');
  201. const startObj = new Date('1970-01-01T' + startTime + 'Z');
  202. const endObj = new Date('1970-01-01T' + endTime + 'Z');
  203. // 比较时间
  204. return timeObj >= startObj && timeObj <= endObj;
  205. }
  206. // 地图配置信息
  207. // const mapKey = config.mapKey; // 地图API密钥
  208. const mapType = ref('tmap'); // 地图类型
  209. const address = ref(''); // 初始化地址
  210. // 打卡定位信息
  211. const longitude = ref(0); // 经度
  212. const latitude = ref(0); // 纬度
  213. // 标记信息
  214. const marker = reactive({
  215. id: 1,
  216. latitude: 25.958812,
  217. longitude: 119.213036,
  218. width: 50, // 标记宽度
  219. height: 50, // 标记高度
  220. title: '宇光同行' // 标记标题
  221. });
  222. // 获取当前定位经纬度
  223. function getlocation() {
  224. return new Promise((resolve, reject) => {
  225. //判断用户是否打开位置权限
  226. uni.getSetting({
  227. success(res) {
  228. // 如果用户未授权
  229. if (!res.authSetting['scope.userLocation']) {
  230. $modal.confirm('需要您授权获取地理位置信息').then(res => {
  231. if (res) {
  232. // 打开权限设置页面
  233. uni.openSetting({
  234. success: (settingData) => {
  235. if (settingData.authSetting[
  236. 'scope.userLocation']) {
  237. $modal.msg('授权后请重新打开此页面')
  238. // reject("用户未授权");
  239. }
  240. }
  241. });
  242. }
  243. })
  244. .catch(err => {
  245. //用户点击取消
  246. if (!err) {
  247. uni.showToast({
  248. title: '获取地理位置授权失败',
  249. icon: 'none',
  250. success: () => {
  251. // 返回上一页
  252. setTimeout(() => {
  253. $tab.navigateBack()
  254. }, 1000);
  255. }
  256. });
  257. // reject("用户未授权");
  258. }
  259. })
  260. } else {
  261. // 已授权,获取位置
  262. fetchLocation(resolve, reject);
  263. }
  264. },
  265. fail(err) {
  266. reject(err); // 拒绝 Promise
  267. }
  268. });
  269. });
  270. }
  271. // 封装获取位置的函数
  272. function fetchLocation(resolve, reject) {
  273. wx.getLocation({
  274. success: (res) => {
  275. longitude.value = res.longitude; // 保存经度
  276. latitude.value = res.latitude; // 保存纬度
  277. resolve(); // 解析 Promise
  278. },
  279. fail: (err) => {
  280. if (err.errMsg.includes("频繁调用")) {
  281. $modal.msg('请勿频繁调用');
  282. // reject("频繁调用错误");
  283. return;
  284. }
  285. console.log('getLocationErr', err);
  286. reject(err); // 拒绝 Promise
  287. },
  288. });
  289. }
  290. // 获取当前位置地址
  291. function getAddress() {
  292. return new Promise((resolve, reject) => {
  293. getlocation()
  294. .then(res => {
  295. tranLocationToAddress(); // 执行地址转换
  296. resolve(res); // 地址获取成功
  297. })
  298. .catch(err => {
  299. // console.error('获取地址失败', err);
  300. reject(err); // 地址获取失败
  301. });
  302. });
  303. }
  304. // 经纬度转地址
  305. function tranLocationToAddress() {
  306. let locationStr = latitude.value + ',' + longitude.value; // 组合经纬度
  307. tranAddress(locationStr).then(res => {
  308. address.value = res.result.address; // 保存地址
  309. })
  310. .catch(err => {
  311. console.log('地址转换请求失败', err); // 请求错误处理
  312. })
  313. }
  314. // 多边形区域设置
  315. const polygons = reactive([{
  316. points: [{
  317. latitude: 25.9591401,
  318. longitude: 119.21292356
  319. },
  320. {
  321. latitude: 25.95828592,
  322. longitude: 119.21261955
  323. },
  324. {
  325. latitude: 25.9576709,
  326. longitude: 119.21458294
  327. },
  328. {
  329. latitude: 25.95845106,
  330. longitude: 119.21486162
  331. },
  332. {
  333. latitude: 25.9591401,
  334. longitude: 119.21292356
  335. }
  336. ],
  337. strokeWidth: 1, // 边框宽度
  338. strokeColor: "#ff000066", // 边框颜色
  339. fillColor: "#ff000016", // 填充颜色
  340. }]);
  341. // 打卡
  342. function clockIn(attType) {
  343. // 发起打卡请求相关业务逻辑
  344. //判断是否已经打过卡
  345. if (isClockIn(attType)) {
  346. return;
  347. }
  348. var now = new Date();
  349. // 使用toISOString()获取一个ISO格式的字符串,然后替换T为空格,并去除最后的Z(表示UTC时间)
  350. var formatted = now.toISOString().replace('T', ' ').replace('Z', '');
  351. var localString = new Date(formatted + '-08:00').toISOString().replace('T', ' ').replace('Z', '');
  352. // console.log('nowtime',formatted,localString);
  353. const params = {
  354. user_name: thisUser.userName,
  355. user_id: thisUser.useId,
  356. kaoqin_type: attType,
  357. now_date: localString,
  358. longitude: longitude.value,
  359. latitude: latitude.value,
  360. address: address.value,
  361. unitId: thisUser.unitId
  362. };
  363. createAttendance(params).then(res => {
  364. if ("success" == res.returnMsg) {
  365. uni.showToast({
  366. title: "打卡成功",
  367. icon: 'none'
  368. });
  369. getTodayAtt(); //更新今日考勤数据
  370. $tab.navigateTo('/pages/mine/checkIn/checkIn')
  371. // return true;
  372. } else {
  373. uni.showToast({
  374. title: "未在指定范围,打卡失败",
  375. icon: 'none'
  376. });
  377. // return false;
  378. }
  379. })
  380. // const _polygons = polygons.map((polygon) => {
  381. // return polygon.points.map((i) => [
  382. // Number(i.longitude),
  383. // Number(i.latitude),
  384. // ]);
  385. // });
  386. // const _point = point([longitude.value, latitude.value]); // 用当前坐标创建点
  387. // const _polygon = polygon(_polygons); // 创建多边形
  388. // const f = booleanPointInPolygon(_point, _polygon); // 判断点是否在多边形内
  389. }
  390. //判断是否已经打卡
  391. function isClockIn(attType) {
  392. var attTypeData = '';
  393. switch (attType) {
  394. case 1:
  395. attTypeData = signInTime.value;
  396. break;
  397. case 2:
  398. break;
  399. }
  400. if ('' !== attTypeData) {
  401. $modal.showToast('请勿重复打卡')
  402. return true;
  403. }
  404. return false;
  405. }
  406. </script>
  407. <style>
  408. @import "@/static/font/ygoa/iconfont.css";
  409. .ygoa-icon {
  410. margin-right: 20rpx;
  411. font-size: 50rpx;
  412. }
  413. .container {
  414. display: flex;
  415. flex-direction: column;
  416. align-items: center;
  417. padding: 20rpx;
  418. }
  419. .header {
  420. width: 100%;
  421. height: 200rpx;
  422. background-color: #f8f8f8;
  423. display: flex;
  424. flex-direction: column;
  425. align-items: center;
  426. justify-content: center;
  427. }
  428. .day {
  429. font-size: 24px;
  430. color: #333;
  431. /* font-weight: bold; */
  432. }
  433. .date {
  434. font-size: 16px;
  435. color: #666;
  436. }
  437. .nowTime {
  438. font-size: 16px;
  439. color: #666;
  440. }
  441. .record {
  442. width: 100%;
  443. margin-top: 20rpx;
  444. border-top: 1px solid #ddd;
  445. border-bottom: 1px solid #ddd;
  446. padding: 0 15rpx;
  447. }
  448. .record-item {
  449. display: flex;
  450. align-items: center;
  451. height: 100rpx;
  452. border-bottom: 1px solid #eee;
  453. }
  454. .icon {
  455. width: 50rpx;
  456. height: 50rpx;
  457. margin-right: 10rpx;
  458. }
  459. .title {
  460. font-size: 28rpx;
  461. }
  462. .time {
  463. font-size: 24rpx;
  464. color: #999;
  465. margin-left: auto;
  466. }
  467. .signStatus {
  468. font-size: 24rpx;
  469. color: orange;
  470. /* margin-left: auto; */
  471. }
  472. .map {
  473. margin-top: 20rpx;
  474. height: 56vh !important;
  475. }
  476. .footer {
  477. width: 100%;
  478. margin-top: 20rpx;
  479. display: flex;
  480. justify-content: space-around;
  481. }
  482. .footer button {
  483. width: 100%;
  484. }
  485. </style>