checkIn.vue 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145
  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">{{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">{{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">{{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">{{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">
  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">
  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. {
  643. "name": "迟到",
  644. "value": myWeekAttData.type4,
  645. "labelText": "迟到:" + myWeekAttData.type4 + "次"
  646. },
  647. {
  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. lateCount.value = myWeekAttData.type4;
  679. leaveEarlyCount.value = myWeekAttData.type5;
  680. absenteeismCount.value = myWeekAttData.type6;
  681. resolve(mockData)
  682. }).catch(error => {
  683. reject(error)
  684. })
  685. })
  686. }
  687. // 获取月考勤记录
  688. function fetchMonthAttData() {
  689. //数据初始化
  690. attendanceCount.value = 0;
  691. goOutCount.value = 0;
  692. lateCount.value = 0;
  693. leaveEarlyCount.value = 0;
  694. absenteeismCount.value = 0;
  695. const params = {
  696. staffId: userId,
  697. now_date: todayData.day
  698. }
  699. getMyTotalMonthCount(params).then(res=>{
  700. const myMonthAttData = res.returnParams;
  701. let data1 = []; // 收集 type6 的值
  702. let data2 = []; // 收集 type4 的值
  703. let data3 = []; // 收集 type5 的值
  704. // console.log('attendanceCount: ',attendanceCount.value);
  705. // 遍历 myMonthAttData 并收集指定类型的值
  706. myMonthAttData.forEach(item => {
  707. data1.push(item.type6);
  708. data2.push(item.type4);
  709. data3.push(item.type5);
  710. attendanceCount.value += item.attDays;
  711. goOutCount.value += item.type2;
  712. lateCount.value += item.type4;
  713. leaveEarlyCount.value += item.type5;
  714. absenteeismCount.value += item.type6;
  715. });
  716. // 定义一个常量数组生成器
  717. const generateWeekArray = n => Array.from({ length: n }, (_, i) => `第${i + 1}周`);
  718. let categoriesArr=generateWeekArray(myMonthAttData.length)
  719. // console.log(categoriesArr);
  720. const mockData = {
  721. categories:  categoriesArr,
  722. series: [{
  723. name: "缺勤",
  724. data: data1
  725. }, {
  726. name: "迟到",
  727. data: data2
  728. }, {
  729. name: "早退",
  730. data: data3
  731. }]
  732. };
  733. updateChart(mockData)
  734. })
  735. }
  736. // //获取六天前的日期
  737. // function getSixDaysAgo(dateStr) {
  738. // const date = new Date(dateStr);
  739. // date.setDate(date.getDate() - 6);
  740. // return date.toISOString().split('T')[0]; // 返回格式为'YYYY-MM-DD'的字符串
  741. // }
  742. // //获取一天前的日期
  743. // function getOneDaysAgo(dateStr) {
  744. // const date = new Date(dateStr);
  745. // date.setDate(date.getDate() - 1);
  746. // return date.toISOString().split('T')[0]; // 返回格式为'YYYY-MM-DD'的字符串
  747. // }
  748. // 更新图表数据
  749. function updateChart(res) {
  750. chartData.value = JSON.parse(JSON.stringify(res)); // 更新图表数据
  751. }
  752. </script>
  753. <style lang="scss" scoped>
  754. // @import "@/static/font/ygoa/iconfont.css";
  755. .ygoa_icon {
  756. margin-right: 0.5rem;
  757. font-size: calc(2rem + 0px) !important;
  758. vertical-align: middle;
  759. /* 确保图标与文本竖直居中 */
  760. }
  761. .attendance-page {
  762. padding: 20px;
  763. background-color: #f9f9f9;
  764. ::v-deep .calendar-content {
  765. margin: -20px;
  766. margin-top: 20px;
  767. .uni-calendar__header {
  768. .uni-calendar__header-text {
  769. font-size: calc(14px + .5*(1rem - 16px)) !important;
  770. }
  771. .uni-calendar__backtoday {
  772. padding: 2px 8px 2px 10px !important;
  773. font-size: calc(0.75rem + 0px) !important;
  774. }
  775. }
  776. .uni-calendar__box {
  777. .uni-calendar__weeks {
  778. .uni-calendar__weeks-day {
  779. .uni-calendar__weeks-day-text {
  780. font-size: calc(14px + .5*(1rem - 16px)) !important;
  781. }
  782. }
  783. .uni-calendar__weeks-item {
  784. .uni-calendar-item__weeks-box-item {
  785. .uni-calendar-item__weeks-box-circle {
  786. width: calc(8px + .5*(1rem - 16px)) !important;
  787. height: calc(8px + .5*(1rem - 16px)) !important;
  788. top: calc(5px - .25*(1rem - 16px)) !important;
  789. right: calc(5px - .25*(1rem - 16px)) !important;
  790. }
  791. .uni-calendar-item__weeks-box-text {
  792. font-size: calc(14px + .5*(1rem - 16px)) !important;
  793. }
  794. .uni-calendar-item__weeks-lunar-text {
  795. font-size: calc(12px + .5*(1rem - 16px)) !important;
  796. }
  797. }
  798. }
  799. }
  800. }
  801. }
  802. }
  803. .header {
  804. display: flex;
  805. justify-content: space-between;
  806. align-items: center;
  807. margin-bottom: 20px;
  808. .clockRecordTitle {
  809. font-size: calc(1.25rem + 0px) !important;
  810. margin: auto;
  811. }
  812. }
  813. .title {
  814. font-size: calc(1.5rem + 0px) !important;
  815. font-weight: bold;
  816. color: #333;
  817. }
  818. .picker {
  819. border: 1px solid #3498db;
  820. border-radius: 5px;
  821. padding: 10px;
  822. background-color: #ecf6fc;
  823. color: #3498db;
  824. }
  825. .statistics {
  826. // display: flex;
  827. justify-content: center;
  828. }
  829. .statistic-card {
  830. background: white;
  831. border-radius: 10px;
  832. padding: 20px;
  833. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  834. /* width: 100%; */
  835. max-width: 600px;
  836. }
  837. .statistic-title {
  838. text-align: center;
  839. font-size: calc(1.25rem + 0px) !important;
  840. font-weight: bold;
  841. color: #333;
  842. }
  843. .statistic-item {
  844. display: flex;
  845. justify-content: space-between;
  846. border-bottom: 1px solid #eee;
  847. padding: 10px 0;
  848. }
  849. .label {
  850. color: #666;
  851. }
  852. .value {
  853. font-size: calc(1.125rem + 0px) !important;
  854. font-weight: bold;
  855. line-height: calc(2rem + 0px) !important;
  856. color: #333;
  857. }
  858. .label {
  859. font-size: calc(1rem + 0px) !important;
  860. line-height: calc(2rem + 0px) !important;
  861. }
  862. .chart-container {
  863. margin-top: 20px;
  864. }
  865. .chart-title {
  866. font-size: calc(1.25rem + 0px) !important;
  867. font-weight: bold;
  868. text-align: center;
  869. margin-bottom: 10px;
  870. color: #333;
  871. }
  872. .charts-box {
  873. width: 100%;
  874. height: 300px;
  875. }
  876. .todayCheckIn {
  877. background-color: #ffffff;
  878. padding: 20px;
  879. margin: auto -20px; //抵消父级padding
  880. box-sizing: border-box;
  881. border-radius: 10px;
  882. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  883. }
  884. .check-in-container {
  885. width: 100%;
  886. height: 100%;
  887. display: flex;
  888. flex-direction: column;
  889. align-items: center;
  890. justify-content: center;
  891. position: relative;
  892. .exceptionText{
  893. /*position: absolute;
  894. top: 10px;
  895. right: 10px;
  896. color: blue;
  897. text-decoration: underline;*/
  898. color: #1890FF;
  899. text-decoration: underline;
  900. font-size: calc(14px + .5*(1rem - 16px));
  901. cursor: pointer;
  902. }
  903. .action-buttons {
  904. position: absolute;
  905. top: 5px;
  906. right: 10px;
  907. display: flex;
  908. flex-direction: column;
  909. align-items: flex-end;
  910. gap: 5px;
  911. }
  912. .viewMoreText {
  913. color: #1890FF;
  914. text-decoration: underline;
  915. font-size: calc(14px + .5*(1rem - 16px));
  916. cursor: pointer;
  917. }
  918. }
  919. .todayAttTitle {
  920. font-size: calc(1.375rem + 0px) !important;
  921. font-weight: bold;
  922. margin-bottom: 20px;
  923. }
  924. .info-row {
  925. width: 100%;
  926. display: flex;
  927. align-items: center;
  928. justify-content: space-between;
  929. margin-bottom: 10px;
  930. .att_status{
  931. color: red;
  932. }
  933. }
  934. .icon {
  935. width: 50rpx;
  936. height: 50rpx;
  937. margin-right: 20rpx;
  938. vertical-align: middle;
  939. /* 确保图标与文本竖直居中 */
  940. }
  941. .clockRecord {
  942. margin-bottom: 20px;
  943. padding: 10px;
  944. border: 1px solid #ccc;
  945. border-radius: 8px;
  946. }
  947. .date {
  948. font-size: 16px;
  949. font-weight: bold;
  950. margin-bottom: 5px;
  951. }
  952. .name {
  953. font-size: 14px;
  954. color: #666;
  955. margin-bottom: 10px;
  956. }
  957. .divider {
  958. border-bottom: 5px dashed #ccc; //虚线
  959. margin: 10px 0;
  960. }
  961. .shift-label {
  962. font-size: 14px;
  963. color: #333;
  964. }
  965. .shift-time {
  966. font-size: 14px;
  967. color: #333;
  968. }
  969. /* 弹窗样式 */
  970. .popup-content {
  971. background: white;
  972. border-radius: 10px 10px 0 0;
  973. overflow: hidden;
  974. }
  975. .popup-header {
  976. display: flex;
  977. justify-content: space-between;
  978. align-items: center;
  979. padding: 15px 20px;
  980. border-bottom: 1px solid #eee;
  981. }
  982. .popup-title {
  983. font-size: calc(16px + .5*(1rem - 16px));
  984. font-weight: bold;
  985. color: #333;
  986. }
  987. .popup-close {
  988. font-size: calc(24px + .5*(1rem - 16px));
  989. color: #999;
  990. cursor: pointer;
  991. padding: 0 5px;
  992. }
  993. .popup-body {
  994. max-height: 60vh;
  995. overflow-y: auto;
  996. padding: 15px 20px;
  997. }
  998. .empty-tip {
  999. text-align: center;
  1000. padding: 30px 0;
  1001. color: #999;
  1002. font-size: calc(14px + .5*(1rem - 16px));
  1003. }
  1004. .record-item {
  1005. padding: 12px 10px;
  1006. border-bottom: 1px solid #f0f0f0;
  1007. }
  1008. .record-item:last-child {
  1009. border-bottom: none;
  1010. }
  1011. .record-info {
  1012. display: flex;
  1013. align-items: center;
  1014. margin-bottom: 8px;
  1015. padding-right: 15px;
  1016. }
  1017. .record-type {
  1018. font-size: calc(14px + .5*(1rem - 16px));
  1019. font-weight: bold;
  1020. color: #333;
  1021. }
  1022. .record-time {
  1023. font-size: calc(14px + .5*(1rem - 16px));
  1024. color: #666;
  1025. margin-left: 30px;
  1026. }
  1027. .record-detail {
  1028. display: flex;
  1029. margin-top: 5px;
  1030. font-size: calc(12px + .5*(1rem - 16px));
  1031. }
  1032. .detail-label {
  1033. color: #999;
  1034. margin-right: 5px;
  1035. }
  1036. .detail-value {
  1037. color: #666;
  1038. flex: 1;
  1039. }
  1040. </style>