189 lines
7.6 KiB
Vue
189 lines
7.6 KiB
Vue
<template>
|
||
<view class="page edge">
|
||
<fy-header title="打卡" />
|
||
<fy-scroll class="edge-scroll" dock @refresh="reload">
|
||
<view v-if="!canPunch" class="deny">当前账号没有打卡权限</view>
|
||
<view v-else class="body">
|
||
<view class="day">
|
||
<text class="date">{{ dateLabel }}</text>
|
||
<text class="shift">班次 {{ rules.workStart }}–{{ rules.workEnd }}</text>
|
||
</view>
|
||
|
||
<view class="slots">
|
||
<view class="slot">
|
||
<view class="dot" :class="{ on: inRec }" />
|
||
<view class="slot-m">
|
||
<text class="lab">上班 {{ rules.workStart }}</text>
|
||
<text class="val" :class="{ ok: inRec }">{{ inRec ? (inRec.time || '') + ' 已打卡' : '未打卡' }}</text>
|
||
</view>
|
||
<text class="tiny">{{ inRec ? (inRec.status || '正常') : '' }}</text>
|
||
</view>
|
||
<view class="slot">
|
||
<view class="dot" :class="{ on: outRec }" />
|
||
<view class="slot-m">
|
||
<text class="lab">下班 {{ rules.workEnd }}</text>
|
||
<text class="val" :class="{ ok: outRec }">{{ outRec ? (outRec.time || '') + ' 已打卡' : '未打卡' }}</text>
|
||
</view>
|
||
<text class="tiny">{{ outRec ? (outRec.status || '正常') : '' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="ring-wrap">
|
||
<view class="ring" :class="{ busy: punching }" @click="doPunch">
|
||
<text class="now">{{ now }}</text>
|
||
<text class="act">{{ punchLabel }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="geo">
|
||
<wd-icon name="location" size="28rpx" color="#1677FF" />
|
||
<text>{{ locName || '将使用手机定位校验打卡范围' }}</text>
|
||
</view>
|
||
|
||
<view class="quick">
|
||
<view class="q" @click="goKind('card')">
|
||
<wd-icon name="edit-1" size="40rpx" color="#1677FF" />
|
||
<text>补卡</text>
|
||
</view>
|
||
<view class="q" @click="goKind('leave')">
|
||
<wd-icon name="calendar" size="40rpx" color="#1677FF" />
|
||
<text>请假</text>
|
||
</view>
|
||
<view class="q" @click="goKind('overtime')">
|
||
<wd-icon name="time" size="40rpx" color="#1677FF" />
|
||
<text>加班</text>
|
||
</view>
|
||
<view class="q" @click="goKind('outing')">
|
||
<wd-icon name="location" size="40rpx" color="#1677FF" />
|
||
<text>外出</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="sec">打卡记录</view>
|
||
<view class="card log">
|
||
<view v-if="!records.length" class="empty">暂无记录</view>
|
||
<view v-for="r in records" :key="r.id || r.time" class="rec">
|
||
<view class="rail" />
|
||
<view class="rec-m">
|
||
<text class="rt">{{ r.title || r.kind || '打卡' }} {{ r.time || r.punchTime || '' }}</text>
|
||
<text class="rl">{{ r.location || r.address || locName || '' }}</text>
|
||
</view>
|
||
<text class="ok">{{ r.status || '正常' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</fy-scroll>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import FyHeader from '../../components/fy-header.vue'
|
||
import FyScroll from '../../components/fy-scroll.vue'
|
||
import { store } from '../../utils/store.js'
|
||
import { clockText, toast } from '../../utils/format.js'
|
||
|
||
export default {
|
||
components: { FyHeader, FyScroll },
|
||
data() {
|
||
return { now: clockText(), timer: 0, punching: false, off: null }
|
||
},
|
||
computed: {
|
||
canPunch() { return store.can('personal', 'punch') },
|
||
rules() { return store.attendRules },
|
||
records() { return store.attendance || [] },
|
||
dateLabel() {
|
||
const d = new Date()
|
||
const w = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||
return (d.getMonth() + 1) + '月' + d.getDate() + '日 ' + w[d.getDay()]
|
||
},
|
||
punchLabel() {
|
||
if (this.punching) return '打卡中'
|
||
if (!this.inRec) return '上班打卡'
|
||
if (!this.outRec) return '下班打卡'
|
||
return '更新打卡'
|
||
},
|
||
locName() {
|
||
const locs = this.rules.locations || []
|
||
return locs[0] ? (locs[0].name || locs[0].title || '') : ''
|
||
},
|
||
inRec() {
|
||
return this.records.find((r) => /上班|in|on/i.test(String(r.title || r.kind || r.type || '')))
|
||
},
|
||
outRec() {
|
||
return this.records.find((r) => /下班|out/i.test(String(r.title || r.kind || r.type || '')))
|
||
}
|
||
},
|
||
onShow() {
|
||
if (store.phase !== 'app') uni.reLaunch({ url: '/pages/login/login' })
|
||
store.refreshAttendance()
|
||
this.timer = setInterval(() => { this.now = clockText() }, 1000)
|
||
this.off = store.on(() => this.$forceUpdate())
|
||
},
|
||
onHide() {
|
||
if (this.timer) clearInterval(this.timer)
|
||
if (this.off) this.off()
|
||
},
|
||
methods: {
|
||
async doPunch() {
|
||
if (this.punching) return
|
||
this.punching = true
|
||
try {
|
||
const msg = await store.punch()
|
||
toast(msg, 'success')
|
||
} catch (e) {
|
||
toast(e.message || '打卡失败')
|
||
} finally {
|
||
this.punching = false
|
||
}
|
||
},
|
||
reload() { store.refreshAttendance() },
|
||
goKind(kind) {
|
||
if (!store.can('personal', 'apply')) return toast('没有发起申请权限')
|
||
uni.navigateTo({ url: '/pages/apply/form?kind=' + kind })
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.body { padding: 8rpx 28rpx 48rpx; }
|
||
.day { display: flex; justify-content: space-between; align-items: baseline; padding: 8rpx 8rpx 20rpx; }
|
||
.date { font-size: 32rpx; font-weight: 650; color: #111827; }
|
||
.shift { font-size: 24rpx; color: #6B7280; }
|
||
.slots { background: #fff; border-radius: 20rpx; padding: 8rpx 8rpx; border: 1rpx solid rgba(17,24,39,0.06); }
|
||
.slot { display: flex; align-items: center; padding: 22rpx 20rpx; }
|
||
.slot + .slot { border-top: 1rpx solid #F0F2F5; }
|
||
.dot { width: 16rpx; height: 16rpx; border-radius: 50%; background: #D1D5DB; margin-right: 16rpx; flex-shrink: 0; }
|
||
.dot.on { background: #1677FF; }
|
||
.slot-m { flex: 1; min-width: 0; }
|
||
.lab { display: block; font-size: 28rpx; font-weight: 600; color: #111827; }
|
||
.val { display: block; font-size: 24rpx; color: #9CA3AF; margin-top: 6rpx; }
|
||
.val.ok { color: #16A34A; }
|
||
.tiny { font-size: 22rpx; color: #16A34A; }
|
||
.ring-wrap { display: flex; justify-content: center; padding: 56rpx 0 28rpx; }
|
||
.ring {
|
||
width: 320rpx; height: 320rpx; border-radius: 50%;
|
||
background: linear-gradient(180deg, #3D8BFF 0%, #1677FF 100%);
|
||
box-shadow: 0 16rpx 40rpx rgba(22, 119, 255, 0.35);
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||
color: #fff;
|
||
}
|
||
.ring.busy { opacity: 0.72; }
|
||
.now { font-size: 44rpx; font-weight: 650; letter-spacing: 1rpx; font-variant-numeric: tabular-nums; }
|
||
.act { margin-top: 10rpx; font-size: 30rpx; }
|
||
.geo { display: flex; align-items: center; justify-content: center; gap: 8rpx; font-size: 24rpx; color: #6B7280; }
|
||
.quick { display: flex; margin-top: 36rpx; background: #fff; border-radius: 20rpx; border: 1rpx solid rgba(17,24,39,0.06); }
|
||
.q { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 10rpx; padding: 28rpx 0; font-size: 24rpx; color: #374151; }
|
||
.sec { margin: 36rpx 8rpx 14rpx; font-size: 28rpx; font-weight: 650; color: #111827; }
|
||
.log { padding: 8rpx 0; }
|
||
.rec { display: flex; align-items: center; padding: 22rpx 24rpx; position: relative; }
|
||
.rec + .rec { border-top: 1rpx solid #F0F2F5; }
|
||
.rail { width: 12rpx; height: 12rpx; border-radius: 50%; background: #1677FF; margin-right: 16rpx; flex-shrink: 0; }
|
||
.rec-m { flex: 1; min-width: 0; }
|
||
.rt { display: block; font-size: 28rpx; font-weight: 600; color: #111827; }
|
||
.rl { display: block; font-size: 22rpx; color: #6B7280; margin-top: 4rpx; }
|
||
.ok { color: #16A34A; font-size: 24rpx; }
|
||
.empty { padding: 40rpx; text-align: center; color: #6B7280; }
|
||
.deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; }
|
||
</style>
|