oa_app仓库初始化

This commit is contained in:
2026-09-23 16:52:48 +08:00
commit 65c5e9f66f
82 changed files with 6490 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
<template>
<view class="page">
<fy-header title="打卡" />
<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>
<view class="btn-primary" @click="doPunch">{{ punching ? '定位中' : (outRec ? '更新打卡' : '打卡') }}</view>
<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>
</view>
</template>
<script>
import FyHeader from '../../components/fy-header.vue'
import { store } from '../../utils/store.js'
import { clockText, toast } from '../../utils/format.js'
export default {
components: { FyHeader },
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
}
},
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: #f2f3ff; border-radius: 16rpx; padding: 36rpx 0; text-align: center; }
.now { display: block; font-size: 68rpx; font-weight: 700; letter-spacing: 2rpx; }
.shift { font-size: 22rpx; color: #434655; }
.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: #434655; display: block; margin-top: 12rpx; }
.val.ok { color: #006c49; font-weight: 700; }
.tiny { font-size: 20rpx; color: #737686; }
.geo { display: block; text-align: center; font-size: 22rpx; color: #434655; margin: 16rpx 0 24rpx; }
.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 #eaedff; }
.rt { display: block; font-size: 28rpx; font-weight: 600; }
.rl { display: block; font-size: 22rpx; color: #737686; }
.ok { color: #006c49; font-size: 24rpx; }
.empty { padding: 40rpx; text-align: center; color: #737686; }
.deny { padding: 120rpx 40rpx; text-align: center; color: #737686; }
</style>