Files
oa_app/pages/tabs/clock.vue
T
2026-09-24 11:41:39 +08:00

196 lines
8.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">弹性 09:00–09:30 / 18:00–18:30</text>
</view>
<view class="lieu">多出工时记调休 {{ lieuText }}</view>
<view class="slots">
<view class="slot">
<view class="dot" :class="{ on: inRec }" />
<view class="slot-m">
<text class="lab">上班 09:00–09:30</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">下班 18:00–18:30</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, flexCreditMinutes, flexCreditText, 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 || '')))
},
lieuText() {
const inn = this.inRec && (this.inRec.time || this.inRec.punchTime)
const out = this.outRec && (this.outRec.time || this.outRec.punchTime)
return flexCreditText(flexCreditMinutes(inn, out))
}
},
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; }
.lieu { margin: 0 8rpx 16rpx; font-size: 24rpx; color: #1677FF; }
.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>