130 lines
5.3 KiB
Vue
130 lines
5.3 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="clock-box">
|
||
<text class="now">{{ now }}</text>
|
||
<text class="shift">班次 {{ rules.workStart }}–{{ rules.workEnd }}{{ locName ? ' · ' + locName : '' }}</text>
|
||
</view>
|
||
<view class="grid">
|
||
<view class="stat card">
|
||
<text class="lab">上班</text>
|
||
<text class="val" :class="{ ok: inRec }">{{ inRec ? inRec.time + ' 已打卡' : '未打卡' }}</text>
|
||
<text class="tiny">{{ inRec ? (inRec.status || '正常') : '待打卡' }}</text>
|
||
</view>
|
||
<view class="stat card">
|
||
<text class="lab">下班</text>
|
||
<text class="val" :class="{ ok: outRec }">{{ outRec ? outRec.time + ' 已打卡' : '未打卡' }}</text>
|
||
<text class="tiny">{{ outRec ? (outRec.status || '正常') : '待打卡' }}</text>
|
||
</view>
|
||
</view>
|
||
<wd-button type="primary" block size="large" :loading="punching" @click="doPunch">{{ outRec ? '更新打卡' : '打卡' }}</wd-button>
|
||
<text class="geo">将使用手机定位校验打卡范围</text>
|
||
<view class="quick">
|
||
<text class="q" @click="goKind('card')">补卡</text>
|
||
<text class="q" @click="goKind('leave')">请假</text>
|
||
<text class="q" @click="goKind('overtime')">加班</text>
|
||
<text class="q" @click="goKind('outing')">外出</text>
|
||
</view>
|
||
<view class="sec">
|
||
<text class="st">今日及近期记录</text>
|
||
</view>
|
||
<view class="card">
|
||
<view v-if="!records.length" class="empty">暂无记录</view>
|
||
<view v-for="r in records" :key="r.id || r.time" class="rec">
|
||
<view>
|
||
<text class="rt">{{ r.title || r.kind || '打卡' }} {{ r.time || r.punchTime || '' }}</text>
|
||
<text class="rl">{{ r.location || r.address || '' }}</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 || [] },
|
||
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: 16rpx 24rpx 40rpx; }
|
||
.clock-box { background: #fff; border-radius: 24rpx; padding: 48rpx 0 40rpx; text-align: center; border: 1rpx solid rgba(17,24,39,0.06); }
|
||
.now { display: block; font-size: 72rpx; font-weight: 650; letter-spacing: 2rpx; color: #111827; }
|
||
.shift { font-size: 22rpx; color: #4B5563; }
|
||
.grid { display: flex; gap: 16rpx; margin: 20rpx 0; }
|
||
.stat { flex: 1; padding: 24rpx; }
|
||
.lab { font-size: 26rpx; font-weight: 600; display: block; }
|
||
.val { font-size: 24rpx; color: #4B5563; display: block; margin-top: 12rpx; }
|
||
.val.ok { color: #16A34A; font-weight: 650; }
|
||
.tiny { font-size: 20rpx; color: #6B7280; }
|
||
.geo { display: block; text-align: center; font-size: 22rpx; color: #4B5563; margin: 16rpx 0 24rpx; }
|
||
.body :deep(.wd-button) { margin-top: 8rpx; }
|
||
.quick { display: flex; gap: 12rpx; }
|
||
.q { flex: 1; height: 72rpx; background: #fff; border-radius: 12rpx; text-align: center; line-height: 72rpx; font-size: 24rpx; }
|
||
.sec { margin: 32rpx 0 12rpx; font-size: 28rpx; font-weight: 600; }
|
||
.rec { display: flex; justify-content: space-between; align-items: center; padding: 24rpx 28rpx; border-bottom: 1rpx solid #E8ECF2; }
|
||
.rt { display: block; font-size: 28rpx; font-weight: 600; }
|
||
.rl { display: block; font-size: 22rpx; color: #6B7280; }
|
||
.ok { color: #16A34A; font-size: 24rpx; }
|
||
.empty { padding: 40rpx; text-align: center; color: #6B7280; }
|
||
.deny { padding: 120rpx 40rpx; text-align: center; color: #6B7280; }
|
||
</style>
|