clockIn.vue 14 KB

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