checkIn.vue 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151
  1. <template>
  2. <page-meta root-font-size="system" />
  3. <view class="attendance-page">
  4. <view class="header">
  5. <text class="title">我的考勤记录</text>
  6. <picker mode="selector" :range="timeRanges" @change="onTimeRangeChange">
  7. <view class="picker">
  8. {{ selectedTimeRange }}/切换
  9. </view>
  10. </picker>
  11. </view>
  12. <!-- 今日签到信息区域 -->
  13. <view class="todayCheckIn" v-if="todayInfoShow">
  14. <view class="check-in-container">
  15. <text class="todayAttTitle">今日签到信息</text>
  16. <view class="action-buttons">
  17. <text class="exceptionText" v-if="lackCardState" @click="toBuKa">处理异常</text>
  18. <text class="viewMoreText" @click="showAllRecords">查看更多</text>
  19. </view>
  20. <view class="info-row">
  21. <view>
  22. <text class="ygoa_icon icon-date"></text>
  23. <text class="value">日期:</text>
  24. </view>
  25. <text class="label">{{ todayData.day }}</text>
  26. </view>
  27. <view class="info-row">
  28. <view>
  29. <text class="ygoa-icon icon-day"></text>
  30. <text class="value" v-if="!clockMultiple">晨签时间:</text>
  31. <text class="value" v-if="clockMultiple">早上签到:</text>
  32. </view>
  33. <view>
  34. <text class="label">{{ todayData.startTime || '未打卡' }}</text>
  35. <text class="att_status" v-if="config.companyCode !== 'yz'">{{todayData.startStatus}}</text>
  36. </view>
  37. </view>
  38. <view class="info-row" v-if="clockMultiple">
  39. <view>
  40. <text class="ygoa-icon icon-day"></text>
  41. <text class="value">早上签退:</text>
  42. </view>
  43. <view>
  44. <text class="label">{{ todayData.endTimeAm || '未打卡' }}</text>
  45. <text class="att_status" v-if="config.companyCode !== 'yz'">{{todayData.endStatusForAm}}</text>
  46. </view>
  47. </view>
  48. <view class="info-row" v-if="clockMultiple">
  49. <view>
  50. <text class="ygoa-icon icon-night"></text>
  51. <text class="value">下午签到:</text>
  52. </view>
  53. <view>
  54. <text class="label">{{ todayData.startTimePm || '未打卡' }}</text>
  55. <text class="att_status" v-if="config.companyCode !== 'yz'">{{todayData.startStatusForPm}}</text>
  56. </view>
  57. </view>
  58. <view class="info-row">
  59. <view>
  60. <text class="ygoa-icon icon-night"></text>
  61. <text class="value" v-if="!clockMultiple">晚签时间:</text>
  62. <text class="value" v-if="clockMultiple">下午签退:</text>
  63. </view>
  64. <view>
  65. <text class="label">{{ todayData.endTime || '未打卡' }}</text>
  66. <text class="att_status" v-if="config.companyCode !== 'yz'">{{todayData.endStatus}}</text>
  67. </view>
  68. </view>
  69. </view>
  70. <!-- 查看所有记录的弹窗 -->
  71. <uni-popup ref="allRecordsPopup" type="bottom">
  72. <view class="popup-content">
  73. <view class="popup-header">
  74. <text class="popup-title">当日所有打卡记录</text>
  75. <text class="popup-close" @click="closeDialog">×</text>
  76. </view>
  77. <scroll-view scroll-y class="popup-body">
  78. <view v-if="allAttList.length === 0" class="empty-tip">
  79. 暂无打卡记录
  80. </view>
  81. <view v-for="(item, index) in allAttList" :key="index" class="record-item">
  82. <view class="record-info">
  83. <text class="record-type">{{ formatAttType(item.att_type_id) }}</text>
  84. <text class="record-time">{{ item.att_time }}</text>
  85. </view>
  86. <view class="record-detail" v-if="item.location">
  87. <text class="detail-label">位置:</text>
  88. <text class="detail-value">{{ item.location }}</text>
  89. </view>
  90. <view class="record-detail" v-if="item.remark">
  91. <text class="detail-label">备注:</text>
  92. <text class="detail-value">{{ item.remark }}</text>
  93. </view>
  94. </view>
  95. </scroll-view>
  96. </view>
  97. </uni-popup>
  98. <!-- 分界线 -->
  99. <view class="divider"></view>
  100. <!-- 打卡记录 -->
  101. <!-- <view class="container">
  102. <view class="header">
  103. <text class="clockRecordTitle">打卡记录</text>
  104. </view>
  105. <view class="content" style="height: 41vh;">
  106. <z-paging :fixed="false" :default-page-size="pSize" default-page-no="1" @query="queryData" ref="paging">
  107. <view class="clockRecord" v-for="clockRecord in clockRecords" :key="clockRecord.date">
  108. <text class="date">{{ clockRecord.date }}</text>
  109. <view class="shifts">
  110. <view class="shift">
  111. <text class="shift-label">晨签:</text>
  112. <text class="shift-time">{{ clockRecord.morning||'没有该记录'}}</text>
  113. </view>
  114. <view class="shift">
  115. <text class="shift-label">晚签:</text>
  116. <text class="shift-time">{{ clockRecord.evening ||'没有该记录'}}</text>
  117. </view>
  118. </view>
  119. </view>
  120. </z-paging>
  121. </view>
  122. </view> -->
  123. <!-- 日历板块 -->
  124. <template>
  125. <view class="calendar-content">
  126. <view>
  127. <uni-calendar class="uni-calendar--hook" :selected="calenderData" :showMonth="true" @change="changeDate"
  128. @monthSwitch="monthSwitch" />
  129. </view>
  130. </view>
  131. </template>
  132. </view>
  133. <!-- 周,月数据统计区域 -->
  134. <view class="statistics" v-if="chartShow">
  135. <view class="statistic-card">
  136. <view class="statistic-title">{{ selectedTimeRange }}考勤统计</view>
  137. <view class="statistic-item">
  138. <view>
  139. <text class="ygoa_icon icon-attendance"></text>
  140. <text class="label">出勤:</text>
  141. </view>
  142. <text class="value">{{ attendanceCount }}</text>
  143. </view>
  144. <view class="statistic-item">
  145. <view>
  146. <text class="ygoa_icon icon-goOut"></text>
  147. <text class="label">外出:</text>
  148. </view>
  149. <text class="value">{{ goOutCount }}</text>
  150. </view>
  151. <view class="statistic-item" v-if="config.companyCode !== 'yz'">
  152. <view>
  153. <text class="ygoa_icon icon-late"></text>
  154. <text class="label">迟到:</text>
  155. </view>
  156. <text class="value">{{ lateCount }}</text>
  157. </view>
  158. <view class="statistic-item" v-if="config.companyCode !== 'yz'">
  159. <view>
  160. <text class="ygoa_icon icon-leaveEarly"></text>
  161. <text class="label">早退:</text>
  162. </view>
  163. <text class="value">{{ leaveEarlyCount }}</text>
  164. </view>
  165. <view class="statistic-item">
  166. <view>
  167. <text class="ygoa_icon icon-absenteeism"></text>
  168. <text class="label">缺勤:</text>
  169. </view>
  170. <text class="value">{{ absenteeismCount }}</text>
  171. </view>
  172. </view>
  173. </view>
  174. <!-- 图表区域 -->
  175. <view class="chart-container" v-if="chartShow">
  176. <view class="">
  177. <text class="chart-title">考勤趋势图</text>
  178. <view class="charts-box">
  179. <qiun-data-charts :type="chartsType" :opts="opts" :chartData="chartData" />
  180. </view>
  181. </view>
  182. </view>
  183. </view>
  184. </template>
  185. <script setup lang="ts">
  186. import {ref,onMounted,reactive} from 'vue';
  187. import { getProcessList } from '@/api/work.js'
  188. import {checkAttendance,getMyTotalCount,getMyQDQtAttendance,getMyTotalMonthCount} from '@/api/mine.js'
  189. import {useUserStore} from '@/store/user.js';
  190. import { useConfigStore } from '@/store/config'
  191. import $tab from '@/plugins/tab.js'
  192. import config from '@/config.js';
  193. import UniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue';
  194. const userStore = useUserStore();
  195. //考勤规则数据
  196. const configStore = useConfigStore();
  197. // 创建一个响应式对象记录今天的考勤数据
  198. const todayData = reactive({
  199. day: '', // 当前日期
  200. startTime: '', // 晨签到时间
  201. startStatus:'',//晨签状态
  202. endTime: '', // 晚签到时间
  203. endStatus:'',//晚签状态
  204. endTimeAm: '',//早上下班签退时间
  205. endStatusForAm:'',//早上下班签退状态
  206. startTimePm: '', // 下午上班签到时间
  207. startStatusForPm:'',//下午上班签到状态
  208. })
  209. // 控制今日签到信息和图表显示的状态
  210. const todayInfoShow = ref(true) // 今日签到信息是否显示
  211. const chartShow = ref(false) // 图表是否显示
  212. // 图表类型的响应式引用
  213. const chartsType = ref('')
  214. // 图表要填充的数据
  215. const chartData = ref({})
  216. // 图表配置选项
  217. const opts = ref({
  218. color: ["#EE6666", "#FAC858", "#FC8452", "#73C0DE", "#3CA272", "#1890FF", "#91CB74", "#9A60B4",
  219. "#ea7ccc"
  220. ], // 设定图表颜色
  221. padding: [15, 15, 0, 5],
  222. enableScroll: false, // 滚动
  223. legend: {}, // 图例设置
  224. xAxis: {
  225. disableGrid: true ,// 禁用网格线
  226. // scrollShow: true,
  227. },
  228. yAxis: {
  229. data: [{
  230. min: 0 // y轴最小值
  231. }]
  232. },
  233. extra: {
  234. column: {
  235. type: "group",
  236. width: 30,
  237. activeBgColor: "#000000",
  238. activeBgOpacity: 0.08
  239. }
  240. }
  241. });
  242. // 时间范围选择数组
  243. const timeRanges = ref(['日', '周', '月']);
  244. const selectedTimeRange = ref('日'); // 初始化选择的时间范围为“日”
  245. // 考勤相关统计数据
  246. const attendanceCount = ref(0); // 出勤次数
  247. const goOutCount = ref(0); // 外出次数
  248. const lateCount = ref(0); // 迟到次数
  249. const leaveEarlyCount = ref(0); // 早退次数
  250. const absenteeismCount = ref(0); // 缺勤次数
  251. //用户id
  252. const userId = userStore.user.useId; //拿到用户id
  253. //当前周的周一日期
  254. const thisMondayDate = ref('');
  255. const clockMultiple = ref(false)
  256. if(config.clock && config.clock == 'multiple'){
  257. clockMultiple.value = true;
  258. }else{
  259. clockMultiple.value = false;
  260. }
  261. onMounted(() => {
  262. initAttData(); //初始化日考勤数据
  263. });
  264. //初始化考勤数据
  265. function initAttData() {
  266. const nowDate = new Date();
  267. todayData.day = formatDate(nowDate);
  268. getthisMondayDate(nowDate);
  269. getDayAttData();
  270. initCalenderData();
  271. }
  272. //格式化日期
  273. function formatDate(date) {
  274. const year = date.getFullYear(); // 获取年份
  275. const month = date.getMonth() + 1; // 获取月份
  276. const day = date.getDate(); // 获取日期
  277. // 格式化月份和日期为两位数
  278. const formattedMonth = month < 10 ? '0' + month : month;
  279. const formattedDay = day < 10 ? '0' + day : day;
  280. return `${year}-${formattedMonth}-${formattedDay}`;
  281. }
  282. //获取日考勤数据
  283. const currentAttList = ref([]) // 保存当前完整的考勤记录列表
  284. function getDayAttData() {
  285. const params = {
  286. universalid: userId,
  287. rizi: todayData.day
  288. }
  289. checkAttendance(params).then(res => {
  290. if ("success" == res.returnMsg) {
  291. //数据初始化
  292. todayData.startTime = ''
  293. todayData.endTime = ''
  294. todayData.startTimePm = ''
  295. todayData.endTimeAm = ''
  296. todayData.startStatus=''
  297. todayData.endStatus=''
  298. todayData.startStatusForPm=''
  299. todayData.endStatusForAm=''
  300. if (res.returnParams.list.length == 0) {
  301. currentAttList.value = []
  302. return
  303. }
  304. const attList = res.returnParams.list;
  305. // 保存完整列表供弹窗使用
  306. currentAttList.value = attList
  307. let time1;
  308. let time2;
  309. let time3;
  310. let time4;
  311. const filteredRecord1 = attList.filter(item => item.att_type_id === '1');
  312. if (filteredRecord1.length > 0) {
  313. time1 = filteredRecord1.reduce((latest, current) => {
  314. const latestTime = new Date(latest.att_time);
  315. const currentTime = new Date(current.att_time);
  316. return (currentTime < latestTime) ? current : latest;
  317. });
  318. }
  319. //上午下班,取最大时间的那个
  320. const filteredRecord3 = attList.filter(item => item.att_type_id === '7');
  321. if (filteredRecord3.length > 0) {
  322. time3 = filteredRecord3.reduce((latest, current) => {
  323. const latestTime = new Date(latest.att_time);
  324. const currentTime = new Date(current.att_time);
  325. return (currentTime > latestTime) ? current : latest;
  326. });
  327. }
  328. //下午上班,取最小时间
  329. const filteredRecord4 = attList.filter(item => item.att_type_id === '8');
  330. if (filteredRecord4.length > 0) {
  331. time4 = filteredRecord4.reduce((latest, current) => {
  332. const latestTime = new Date(latest.att_time);
  333. const currentTime = new Date(current.att_time);
  334. return (currentTime < latestTime) ? current : latest;
  335. });
  336. }
  337. //拿到所有签退数据后,取最大时间的那个
  338. const filteredRecord2 = attList.filter(item => item.att_type_id === '2');
  339. if (filteredRecord2.length > 0) {
  340. time2 = filteredRecord2.reduce((latest, current) => {
  341. const latestTime = new Date(latest.att_time);
  342. const currentTime = new Date(current.att_time);
  343. return (currentTime > latestTime) ? current : latest;
  344. });
  345. }
  346. if (time1 !== undefined) {
  347. todayData.startTime = time1.att_time.split(' ')[1];
  348. if(subOneMinute(todayData.startTime)>=configStore.signInTimeRange[0][1]){
  349. todayData.startStatus='(迟到)'
  350. }
  351. }
  352. if (time3 !== undefined) {
  353. todayData.endTimeAm = time3.att_time.split(' ')[1];
  354. if(todayData.endTimeAm<configStore.signOutTimeRange[0][0]){
  355. todayData.endStatusForAm='(早退)'
  356. }
  357. }
  358. if (time4 !== undefined) {
  359. todayData.startTimePm = time4.att_time.split(' ')[1];
  360. if(subOneMinute(todayData.startTimePm)>=configStore.signInTimeRange[1][1]){
  361. todayData.startStatusForPm='(迟到)'
  362. }
  363. }
  364. if (time2 !== undefined) {
  365. todayData.endTime = time2.att_time.split(' ')[1];
  366. if(todayData.endTime<configStore.signOutTimeRange[1][0]){
  367. todayData.endStatus='(早退)'
  368. }
  369. }
  370. }
  371. })
  372. }
  373. //减一分钟
  374. function subOneMinute(timeStr) {
  375. // 将时间字符串转换为 Date 对象
  376. const [hours, minutes, seconds] = timeStr.split(':').map(Number);
  377. const date = new Date();
  378. date.setHours(hours, minutes, seconds, 0); // 设置时间为给定的时分秒
  379. // 减去一分钟
  380. date.setMinutes(date.getMinutes() - 1);
  381. return date.toTimeString().slice(0, 8);
  382. }
  383. //打卡历史记录(日历版)
  384. //日历表数据
  385. const calenderData = ref([])
  386. // 初始化此月日历数据
  387. function initCalenderData() {
  388. const params = {
  389. type: 2, //设置时间区间
  390. universalid: userId,
  391. ks_att_time: todayData.day.slice(0, 8) + '01',
  392. js_att_time: todayData.day,
  393. pSize: parseInt(todayData.day.slice(8, 10), 10),//当前天数
  394. p: 1
  395. }
  396. getCalenderData(params)
  397. }
  398. //日历数据的刷新
  399. function getCalenderData(params){
  400. getMyQDQtAttendance(params).then(({ returnParams }) => {
  401. //定义一个数组用于接收日历data
  402. const thisCalenderData=[]
  403. returnParams.list.forEach(item => {
  404. if ((!item.type.includes('1') || !item.type.includes('2'))&&item.att_date.slice(0, 10)!==todayData.day) {
  405. const calenderDataInfo = {
  406. date: item.att_date.slice(0, 10),
  407. info: '缺卡'
  408. }
  409. thisCalenderData.push(calenderDataInfo)
  410. }
  411. });
  412. calenderData.value=thisCalenderData
  413. })
  414. }
  415. const lackCardState=ref(false)//缺卡异常处理状态
  416. // 查看所有记录的弹窗
  417. const allRecordsPopup = ref(null)
  418. const showAllRecordsDialog = ref(false)
  419. const allAttList = ref([]) // 弹窗中显示的列表
  420. // 显示所有考勤记录弹窗 - 直接使用已获取的数据
  421. function showAllRecords() {
  422. allAttList.value = currentAttList.value || []
  423. showAllRecordsDialog.value = true
  424. if (allRecordsPopup.value) {
  425. allRecordsPopup.value.open()
  426. }
  427. }
  428. // 格式化考勤类型
  429. function formatAttType(typeId) {
  430. const typeMap = {
  431. '1': '上午上班',
  432. '7': '上午下班',
  433. '8': '下午上班',
  434. '2': '下午下班'
  435. }
  436. return typeMap[typeId] || '未知类型'
  437. }
  438. // 关闭弹窗
  439. function closeDialog() {
  440. showAllRecordsDialog.value = false
  441. if (allRecordsPopup.value) {
  442. allRecordsPopup.value.close()
  443. }
  444. }
  445. //日历点击日期切换事件
  446. function changeDate(e) {
  447. // console.log('changeDate: ',e);
  448. // console.log("3:calenderData.value " + JSON.stringify(calenderData.value));
  449. if(config.companyCode && config.companyCode == 'yg'){
  450. lackCardState.value = !!e.extraInfo.info;//!!转布尔值
  451. }
  452. todayData.day = e.fulldate
  453. getDayAttData()
  454. }
  455. //补卡页面跳转
  456. function toBuKa(){
  457. const bukaType=todayData.startTime?'下班':'上班'
  458. getProcessList(userStore.user.useId, userStore.user.unitId).then(res => {
  459. const { modelName, modelId, control } = res.returnParams.fList.find(item => item.modelName === "补卡申请")
  460. $tab.navigateTo('/pages/work/edit/index?modelName=' + modelName + '&modelId=' + modelId + '&control=' + control+'&bukaDate='+todayData.day+'&bukaType='+bukaType)
  461. })
  462. // $tab.switchTab('/pages/tabbar-work/index?bukaDate='+todayData.day+'&bukaType='+bukaType)
  463. }
  464. //传入年份月份,返回此月天数
  465. function getDaysInMonth(year, month) {
  466. const date = new Date(year, month - 1, 1);
  467. // 将日期设置为下一个月的第0天,即当前月的最后一天
  468. date.setMonth(date.getMonth() + 1);
  469. date.setDate(0);
  470. // 返回当前月的最后一天的日期,即该月的天数
  471. return date.getDate();
  472. }
  473. //月份切换事件
  474. function monthSwitch(e) {
  475. const days=getDaysInMonth(e.year,e.month);
  476. //月份小于10前面补零
  477. const month = String(e.month).padStart(2, '0');
  478. const params = {
  479. type: 2, //设置时间区间
  480. universalid: userId,
  481. ks_att_time: e.year+'-'+month+'-' + '01',
  482. js_att_time: e.year+'-'+month+'-' +days,
  483. pSize: days,
  484. p: 1
  485. }
  486. getCalenderData(params);
  487. }
  488. // //打卡历史记录(下拉刷新版)
  489. // const paging = ref(null)
  490. // const pSize = ref(5)
  491. // function complete(list, total, pageNo) {
  492. // if (pageNo == 1) {
  493. // paging.value.complete(list);
  494. // return
  495. // }
  496. // // 防止重复获取最后一次信息
  497. // if (pSize.value * pageNo < total) {
  498. // paging.value.complete(list)
  499. // } else {
  500. // paging.value.complete([])
  501. // }
  502. // }
  503. // //日考勤历史记录
  504. // const clockRecords = ref([])
  505. // const totalPage = ref(0)
  506. // //获取日考勤历史(下拉刷新)
  507. // function getDayAttHistory() {
  508. // const params = {
  509. // type: 1, //不设时间区间
  510. // universalid: userId,
  511. // ks_att_time: '',
  512. // js_att_time: '',
  513. // pSize: pSize.value,
  514. // p: 1
  515. // }
  516. // getMyQDQtAttendance(params).then(({ returnParams }) => {
  517. // totalPage.value = Math.ceil(returnParams.total / pSize.value)
  518. // const list = returnParams.list.map(item => {
  519. // return {
  520. // date: item.att_date.substring(0, 10),
  521. // morning: item.time.split(',')[item.type.split(',').indexOf("1")],
  522. // evening: item.time.split(',')[item.type.split(',').indexOf("2")],
  523. // }
  524. // });
  525. // clockRecords.value = list;
  526. // complete(clockRecords.value, returnParams.total, returnParams.current)
  527. // })
  528. // }
  529. // // 刷新
  530. // function queryData(pageNo, pSize, queryType) {
  531. // switch (queryType) {
  532. // case 0: // 下拉刷新
  533. // case 1: // 初始加载
  534. // getDayAttHistory()
  535. // break
  536. // case 3: // 上拉加载
  537. // scrollQuery(pageNo, pSize)
  538. // break
  539. // default: // 默认刷新
  540. // getDayAttHistory()
  541. // break
  542. // }
  543. // }
  544. // //上拉加载事件
  545. // function scrollQuery(pageNo, pSize) {
  546. // const params = {
  547. // type: 1, //不设时间区间
  548. // universalid: userId,
  549. // ks_att_time: '',
  550. // js_att_time: '',
  551. // pSize: pSize,
  552. // p: pageNo
  553. // }
  554. // getMyQDQtAttendance(params).then(({ returnParams }) => {
  555. // const list = returnParams.list.map(item => {
  556. // return {
  557. // date: item.att_date.substring(0, 10),
  558. // morning: item.time.split(',')[item.type.split(',').indexOf("1")],
  559. // evening: item.time.split(',')[item.type.split(',').indexOf("2")],
  560. // }
  561. // });
  562. // if (pageNo <= totalPage.value) {
  563. // clockRecords.value.push(...list);
  564. // }
  565. // complete(list, returnParams.total, pageNo)
  566. // })
  567. // }
  568. // 日周月时间范围切换事件
  569. function onTimeRangeChange(event) {
  570. const selectedIndex = event.detail.value;
  571. selectedTimeRange.value = timeRanges.value[selectedIndex]; // 设置选择的时间范围(日周月)
  572. // 根据选择的时间范围更新展示内容
  573. switch (selectedTimeRange.value) {
  574. case '日':
  575. getDayAttData();
  576. showDay(); // 显示今日考勤
  577. break;
  578. case '周':
  579. chartsType.value = 'pie'; // 设置图表类型为饼图
  580. fetchWeekAttData().then(message => {
  581. // console.log(message);
  582. showWeekAndMonth();
  583. updateChart(message);
  584. })
  585. .catch(error => {
  586. console.error(error);
  587. });
  588. break;
  589. case '月':
  590. chartsType.value = 'column'; // 设置图表类型为柱状图
  591. fetchMonthAttData();
  592. showWeekAndMonth();
  593. // fetchMonthAttData().then(message => {
  594. // console.log('message', message);
  595. // showWeekAndMonth();
  596. // updateChart(message);
  597. // })
  598. // .catch(error => {
  599. // console.error(error);
  600. // });
  601. break;
  602. }
  603. };
  604. // 显示今日考勤数据
  605. function showDay() {
  606. todayInfoShow.value = true; // 显示今日签到信息
  607. chartShow.value = false; // 隐藏图表
  608. }
  609. // 显示周或者月考勤
  610. function showWeekAndMonth() {
  611. todayInfoShow.value = false; // 隐藏今日签到信息
  612. chartShow.value = true; // 显示图表
  613. }
  614. //计算本周周一日期
  615. function getthisMondayDate(today) {
  616. const dayOfWeek = today.getDay(); // 0 (周日) 到 6 (周六)
  617. const dayOfMonth = today.getDate();
  618. // 计算本周周一的日期
  619. const offset = dayOfWeek === 0 ? -6 : 1; // 如果今天是周日,则偏移量为-6,否则为1
  620. const thisMonday = new Date(today);
  621. thisMonday.setDate(dayOfMonth - dayOfWeek + offset);
  622. thisMondayDate.value = formatDate(thisMonday);
  623. }
  624. //获取周考勤记录
  625. function fetchWeekAttData() {
  626. return new Promise((resolve, reject) => {
  627. const params = {
  628. staffId: userId,
  629. start_date: thisMondayDate.value,
  630. end_date: todayData.day
  631. }
  632. getMyTotalCount(params).then(res => {
  633. const myWeekAttData = res.returnParams;
  634. const mockData = {
  635. series: [{
  636. data: [
  637. {
  638. "name": "缺勤",
  639. "value": myWeekAttData.type6,
  640. "labelText": "缺勤:" + myWeekAttData.type6 + "次"
  641. },
  642. ...(config.companyCode !== 'yz' ? [{
  643. "name": "迟到",
  644. "value": myWeekAttData.type4,
  645. "labelText": "迟到:" + myWeekAttData.type4 + "次"
  646. }] : []),
  647. ...(config.companyCode !== 'yz' ? [{
  648. "name": "早退",
  649. "value": myWeekAttData.type5,
  650. "labelText": "早退:" + myWeekAttData.type5 + "次"
  651. }] : []),
  652. {
  653. "name": "出勤",
  654. "value": myWeekAttData.attDays,
  655. "labelText": "出勤:" + myWeekAttData.attDays + "次"
  656. },
  657. {
  658. "name": "外出",
  659. "value": myWeekAttData.type2,
  660. "labelText": "外出:" + myWeekAttData.type2 + "次"
  661. },
  662. {
  663. "name": "公休",
  664. "value": myWeekAttData.type1,
  665. "labelText": "公休:" + myWeekAttData.type1 + "次"
  666. },
  667. // {
  668. // "name": "未排班",
  669. // "value": myWeekAttData.type3,
  670. // "labelText": "未排班:" + myWeekAttData.type3 + "次"
  671. // }
  672. ]
  673. }],
  674. };
  675. // const weekData = mockData.series[0].data;
  676. attendanceCount.value = myWeekAttData.attDays;
  677. goOutCount.value = myWeekAttData.type2;
  678. if(config.companyCode !== 'yz') {
  679. lateCount.value = myWeekAttData.type4;
  680. leaveEarlyCount.value = myWeekAttData.type5;
  681. }
  682. absenteeismCount.value = myWeekAttData.type6;
  683. resolve(mockData)
  684. }).catch(error => {
  685. reject(error)
  686. })
  687. })
  688. }
  689. // 获取月考勤记录
  690. function fetchMonthAttData() {
  691. //数据初始化
  692. attendanceCount.value = 0;
  693. goOutCount.value = 0;
  694. lateCount.value = 0;
  695. leaveEarlyCount.value = 0;
  696. absenteeismCount.value = 0;
  697. const params = {
  698. staffId: userId,
  699. now_date: todayData.day
  700. }
  701. getMyTotalMonthCount(params).then(res=>{
  702. const myMonthAttData = res.returnParams;
  703. let data1 = []; // 收集 type6 的值
  704. let data2 = []; // 收集 type4 的值
  705. let data3 = []; // 收集 type5 的值
  706. // console.log('attendanceCount: ',attendanceCount.value);
  707. // 遍历 myMonthAttData 并收集指定类型的值
  708. myMonthAttData.forEach(item => {
  709. data1.push(item.type6);
  710. if(config.companyCode !== 'yz') {
  711. data2.push(item.type4);
  712. data3.push(item.type5);
  713. }
  714. attendanceCount.value += item.attDays;
  715. goOutCount.value += item.type2;
  716. if(config.companyCode !== 'yz') {
  717. lateCount.value += item.type4;
  718. leaveEarlyCount.value += item.type5;
  719. }
  720. absenteeismCount.value += item.type6;
  721. });
  722. // 定义一个常量数组生成器
  723. const generateWeekArray = n => Array.from({ length: n }, (_, i) => `第${i + 1}周`);
  724. let categoriesArr=generateWeekArray(myMonthAttData.length)
  725. // console.log(categoriesArr);
  726. const mockData = {
  727. categories: categoriesArr,
  728. series: [{
  729. name: "缺勤",
  730. data: data1
  731. }, ...(config.companyCode !== 'yz' ? [{
  732. name: "迟到",
  733. data: data2
  734. }, {
  735. name: "早退",
  736. data: data3
  737. }] : [])]
  738. };
  739. updateChart(mockData)
  740. })
  741. }
  742. // //获取六天前的日期
  743. // function getSixDaysAgo(dateStr) {
  744. // const date = new Date(dateStr);
  745. // date.setDate(date.getDate() - 6);
  746. // return date.toISOString().split('T')[0]; // 返回格式为'YYYY-MM-DD'的字符串
  747. // }
  748. // //获取一天前的日期
  749. // function getOneDaysAgo(dateStr) {
  750. // const date = new Date(dateStr);
  751. // date.setDate(date.getDate() - 1);
  752. // return date.toISOString().split('T')[0]; // 返回格式为'YYYY-MM-DD'的字符串
  753. // }
  754. // 更新图表数据
  755. function updateChart(res) {
  756. chartData.value = JSON.parse(JSON.stringify(res)); // 更新图表数据
  757. }
  758. </script>
  759. <style lang="scss" scoped>
  760. // @import "@/static/font/ygoa/iconfont.css";
  761. .ygoa_icon {
  762. margin-right: 0.5rem;
  763. font-size: calc(2rem + 0px) !important;
  764. vertical-align: middle;
  765. /* 确保图标与文本竖直居中 */
  766. }
  767. .attendance-page {
  768. padding: 20px;
  769. background-color: #f9f9f9;
  770. ::v-deep .calendar-content {
  771. margin: -20px;
  772. margin-top: 20px;
  773. .uni-calendar__header {
  774. .uni-calendar__header-text {
  775. font-size: calc(14px + .5*(1rem - 16px)) !important;
  776. }
  777. .uni-calendar__backtoday {
  778. padding: 2px 8px 2px 10px !important;
  779. font-size: calc(0.75rem + 0px) !important;
  780. }
  781. }
  782. .uni-calendar__box {
  783. .uni-calendar__weeks {
  784. .uni-calendar__weeks-day {
  785. .uni-calendar__weeks-day-text {
  786. font-size: calc(14px + .5*(1rem - 16px)) !important;
  787. }
  788. }
  789. .uni-calendar__weeks-item {
  790. .uni-calendar-item__weeks-box-item {
  791. .uni-calendar-item__weeks-box-circle {
  792. width: calc(8px + .5*(1rem - 16px)) !important;
  793. height: calc(8px + .5*(1rem - 16px)) !important;
  794. top: calc(5px - .25*(1rem - 16px)) !important;
  795. right: calc(5px - .25*(1rem - 16px)) !important;
  796. }
  797. .uni-calendar-item__weeks-box-text {
  798. font-size: calc(14px + .5*(1rem - 16px)) !important;
  799. }
  800. .uni-calendar-item__weeks-lunar-text {
  801. font-size: calc(12px + .5*(1rem - 16px)) !important;
  802. }
  803. }
  804. }
  805. }
  806. }
  807. }
  808. }
  809. .header {
  810. display: flex;
  811. justify-content: space-between;
  812. align-items: center;
  813. margin-bottom: 20px;
  814. .clockRecordTitle {
  815. font-size: calc(1.25rem + 0px) !important;
  816. margin: auto;
  817. }
  818. }
  819. .title {
  820. font-size: calc(1.5rem + 0px) !important;
  821. font-weight: bold;
  822. color: #333;
  823. }
  824. .picker {
  825. border: 1px solid #3498db;
  826. border-radius: 5px;
  827. padding: 10px;
  828. background-color: #ecf6fc;
  829. color: #3498db;
  830. }
  831. .statistics {
  832. // display: flex;
  833. justify-content: center;
  834. }
  835. .statistic-card {
  836. background: white;
  837. border-radius: 10px;
  838. padding: 20px;
  839. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  840. /* width: 100%; */
  841. max-width: 600px;
  842. }
  843. .statistic-title {
  844. text-align: center;
  845. font-size: calc(1.25rem + 0px) !important;
  846. font-weight: bold;
  847. color: #333;
  848. }
  849. .statistic-item {
  850. display: flex;
  851. justify-content: space-between;
  852. border-bottom: 1px solid #eee;
  853. padding: 10px 0;
  854. }
  855. .label {
  856. color: #666;
  857. }
  858. .value {
  859. font-size: calc(1.125rem + 0px) !important;
  860. font-weight: bold;
  861. line-height: calc(2rem + 0px) !important;
  862. color: #333;
  863. }
  864. .label {
  865. font-size: calc(1rem + 0px) !important;
  866. line-height: calc(2rem + 0px) !important;
  867. }
  868. .chart-container {
  869. margin-top: 20px;
  870. }
  871. .chart-title {
  872. font-size: calc(1.25rem + 0px) !important;
  873. font-weight: bold;
  874. text-align: center;
  875. margin-bottom: 10px;
  876. color: #333;
  877. }
  878. .charts-box {
  879. width: 100%;
  880. height: 300px;
  881. }
  882. .todayCheckIn {
  883. background-color: #ffffff;
  884. padding: 20px;
  885. margin: auto -20px; //抵消父级padding
  886. box-sizing: border-box;
  887. border-radius: 10px;
  888. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  889. }
  890. .check-in-container {
  891. width: 100%;
  892. height: 100%;
  893. display: flex;
  894. flex-direction: column;
  895. align-items: center;
  896. justify-content: center;
  897. position: relative;
  898. .exceptionText{
  899. /*position: absolute;
  900. top: 10px;
  901. right: 10px;
  902. color: blue;
  903. text-decoration: underline;*/
  904. color: #1890FF;
  905. text-decoration: underline;
  906. font-size: calc(14px + .5*(1rem - 16px));
  907. cursor: pointer;
  908. }
  909. .action-buttons {
  910. position: absolute;
  911. top: 5px;
  912. right: 10px;
  913. display: flex;
  914. flex-direction: column;
  915. align-items: flex-end;
  916. gap: 5px;
  917. }
  918. .viewMoreText {
  919. color: #1890FF;
  920. text-decoration: underline;
  921. font-size: calc(14px + .5*(1rem - 16px));
  922. cursor: pointer;
  923. }
  924. }
  925. .todayAttTitle {
  926. font-size: calc(1.375rem + 0px) !important;
  927. font-weight: bold;
  928. margin-bottom: 20px;
  929. }
  930. .info-row {
  931. width: 100%;
  932. display: flex;
  933. align-items: center;
  934. justify-content: space-between;
  935. margin-bottom: 10px;
  936. .att_status{
  937. color: red;
  938. }
  939. }
  940. .icon {
  941. width: 50rpx;
  942. height: 50rpx;
  943. margin-right: 20rpx;
  944. vertical-align: middle;
  945. /* 确保图标与文本竖直居中 */
  946. }
  947. .clockRecord {
  948. margin-bottom: 20px;
  949. padding: 10px;
  950. border: 1px solid #ccc;
  951. border-radius: 8px;
  952. }
  953. .date {
  954. font-size: 16px;
  955. font-weight: bold;
  956. margin-bottom: 5px;
  957. }
  958. .name {
  959. font-size: 14px;
  960. color: #666;
  961. margin-bottom: 10px;
  962. }
  963. .divider {
  964. border-bottom: 5px dashed #ccc; //虚线
  965. margin: 10px 0;
  966. }
  967. .shift-label {
  968. font-size: 14px;
  969. color: #333;
  970. }
  971. .shift-time {
  972. font-size: 14px;
  973. color: #333;
  974. }
  975. /* 弹窗样式 */
  976. .popup-content {
  977. background: white;
  978. border-radius: 10px 10px 0 0;
  979. overflow: hidden;
  980. }
  981. .popup-header {
  982. display: flex;
  983. justify-content: space-between;
  984. align-items: center;
  985. padding: 15px 20px;
  986. border-bottom: 1px solid #eee;
  987. }
  988. .popup-title {
  989. font-size: calc(16px + .5*(1rem - 16px));
  990. font-weight: bold;
  991. color: #333;
  992. }
  993. .popup-close {
  994. font-size: calc(24px + .5*(1rem - 16px));
  995. color: #999;
  996. cursor: pointer;
  997. padding: 0 5px;
  998. }
  999. .popup-body {
  1000. max-height: 60vh;
  1001. overflow-y: auto;
  1002. padding: 15px 20px;
  1003. }
  1004. .empty-tip {
  1005. text-align: center;
  1006. padding: 30px 0;
  1007. color: #999;
  1008. font-size: calc(14px + .5*(1rem - 16px));
  1009. }
  1010. .record-item {
  1011. padding: 12px 10px;
  1012. border-bottom: 1px solid #f0f0f0;
  1013. }
  1014. .record-item:last-child {
  1015. border-bottom: none;
  1016. }
  1017. .record-info {
  1018. display: flex;
  1019. align-items: center;
  1020. margin-bottom: 8px;
  1021. padding-right: 15px;
  1022. }
  1023. .record-type {
  1024. font-size: calc(14px + .5*(1rem - 16px));
  1025. font-weight: bold;
  1026. color: #333;
  1027. }
  1028. .record-time {
  1029. font-size: calc(14px + .5*(1rem - 16px));
  1030. color: #666;
  1031. margin-left: 30px;
  1032. }
  1033. .record-detail {
  1034. display: flex;
  1035. margin-top: 5px;
  1036. font-size: calc(12px + .5*(1rem - 16px));
  1037. }
  1038. .detail-label {
  1039. color: #999;
  1040. margin-right: 5px;
  1041. }
  1042. .detail-value {
  1043. color: #666;
  1044. flex: 1;
  1045. }
  1046. </style>