JhHardwareWRS/pages/workSchedule/workSchedule.js
2026-06-24 15:19:14 +08:00

480 lines
16 KiB
JavaScript

const api = require('../../utils/api')
const DEFAULT_POINT_NAME = '宁波嘉恒智能科技有限公司'
const defaultForm = {
dayStart: '08:00',
dayEnd: '17:20',
lunchStart: '11:40',
lunchEnd: '12:40',
dinnerStart: '17:20',
dinnerEnd: '18:00',
overtimeStart: '18:00',
overtimeEnd: '20:00',
nightStart: '20:00',
nightEnd: '06:00',
attendanceLatitude: '',
attendanceLongitude: '',
attendanceRadiusMeters: 500,
autoSubmitHours: 15,
attendancePoints: [],
}
const scheduleFields = [
'dayStart',
'dayEnd',
'lunchStart',
'lunchEnd',
'dinnerStart',
'dinnerEnd',
'overtimeStart',
'overtimeEnd',
'nightStart',
'nightEnd',
]
const defaultScheduleValues = () => scheduleFields.reduce((result, field) => ({
...result,
[field]: defaultForm[field],
}), {})
const emptyPoint = () => ({
localKey: `new_${Date.now()}`,
name: '',
originalName: '',
latitude: '',
longitude: '',
radiusMeters: 500,
...defaultScheduleValues(),
remark: '',
isActive: true,
})
Page({
data: {
form: { ...defaultForm },
updatedAtText: '',
refreshing: false,
saving: false,
locating: false,
locationAccuracyText: '',
expandedPointKey: '',
},
onShow() {
this.load()
},
async load() {
try {
const schedule = await api.getWorkSchedule()
this.setData({
form: this.buildForm(schedule),
updatedAtText: schedule.updatedAtText || '',
expandedPointKey: '',
})
} catch (error) {
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
}
},
async onPullDownRefresh() {
this.setData({ refreshing: true })
try {
await this.load()
} finally {
this.setData({ refreshing: false })
if (wx.stopPullDownRefresh) {
wx.stopPullDownRefresh()
}
}
},
onTimeChange(e) {
const field = e.currentTarget.dataset.field
const pointIndex = e.currentTarget.dataset.index
if (pointIndex !== undefined && pointIndex !== '') {
this.setData({
[`form.attendancePoints[${pointIndex}].${field}`]: e.detail.value,
})
return
}
this.setData({ [`form.${field}`]: e.detail.value })
},
buildForm(schedule = {}) {
return {
dayStart: schedule.dayStart || defaultForm.dayStart,
dayEnd: schedule.dayEnd || defaultForm.dayEnd,
lunchStart: schedule.lunchStart || defaultForm.lunchStart,
lunchEnd: schedule.lunchEnd || defaultForm.lunchEnd,
dinnerStart: schedule.dinnerStart || defaultForm.dinnerStart,
dinnerEnd: schedule.dinnerEnd || defaultForm.dinnerEnd,
overtimeStart: schedule.overtimeStart || defaultForm.overtimeStart,
overtimeEnd: schedule.overtimeEnd || defaultForm.overtimeEnd,
nightStart: schedule.nightStart || defaultForm.nightStart,
nightEnd: schedule.nightEnd || defaultForm.nightEnd,
attendanceLatitude: schedule.attendanceLatitude === '' || schedule.attendanceLatitude === undefined || schedule.attendanceLatitude === null
? ''
: this.formatCoordinate(schedule.attendanceLatitude),
attendanceLongitude: schedule.attendanceLongitude === '' || schedule.attendanceLongitude === undefined || schedule.attendanceLongitude === null
? ''
: this.formatCoordinate(schedule.attendanceLongitude),
attendanceRadiusMeters: schedule.attendanceRadiusMeters || defaultForm.attendanceRadiusMeters,
autoSubmitHours: schedule.autoSubmitHours || defaultForm.autoSubmitHours,
attendancePoints: this.buildAttendancePoints(schedule),
}
},
buildPointSchedule(point = {}, schedule = {}) {
return scheduleFields.reduce((result, field) => ({
...result,
[field]: point[field] || schedule[field] || defaultForm[field],
}), {})
},
buildAttendancePoints(schedule = {}) {
const source = (schedule.attendancePoints || []).length
? schedule.attendancePoints
: [{
name: DEFAULT_POINT_NAME,
latitude: schedule.attendanceLatitude,
longitude: schedule.attendanceLongitude,
radiusMeters: schedule.attendanceRadiusMeters || defaultForm.attendanceRadiusMeters,
remark: '',
isActive: true,
}]
return source.map((point, index) => ({
localKey: point.name || `point_${index}`,
name: point.name || '',
originalName: point.originalName || point.name || '',
latitude: point.latitude === '' || point.latitude === undefined || point.latitude === null ? '' : this.formatCoordinate(point.latitude),
longitude: point.longitude === '' || point.longitude === undefined || point.longitude === null ? '' : this.formatCoordinate(point.longitude),
radiusMeters: point.radiusMeters || defaultForm.attendanceRadiusMeters,
...this.buildPointSchedule(point, schedule),
remark: point.remark || '',
isActive: point.isActive !== false,
}))
},
onRadiusInput(e) {
this.setData({
'form.attendanceRadiusMeters': e.detail.value,
})
},
onAutoSubmitHoursInput(e) {
this.setData({
'form.autoSubmitHours': e.detail.value,
})
},
onCoordinateInput(e) {
const field = e.currentTarget.dataset.field
this.setData({
[`form.${field}`]: e.detail.value,
})
},
onPointInput(e) {
const index = e.currentTarget.dataset.index
const field = e.currentTarget.dataset.field
this.setData({
[`form.attendancePoints[${index}].${field}`]: e.detail.value,
})
},
addAttendancePoint() {
const point = emptyPoint()
this.setData({
'form.attendancePoints': this.data.form.attendancePoints.concat(point),
expandedPointKey: point.localKey,
})
},
toggleAttendancePoint(e) {
const key = e.currentTarget.dataset.key
this.setData({
expandedPointKey: this.data.expandedPointKey === key ? '' : key,
})
},
formatCoordinate(value) {
const number = Number(value)
return Number.isFinite(number) ? number.toFixed(6) : ''
},
formatAccuracy(value) {
const number = Number(value)
if (!Number.isFinite(number)) {
return ''
}
return number < 10 ? `${number.toFixed(1)}` : `${Math.round(number)}`
},
sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
},
requestHighAccuracyLocation() {
return new Promise((resolve, reject) => {
wx.getLocation({
type: 'gcj02',
isHighAccuracy: true,
highAccuracyExpireTime: 8000,
success: resolve,
fail: reject,
})
})
},
normalizeLocation(res) {
const latitude = Number(res && res.latitude)
const longitude = Number(res && res.longitude)
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
return null
}
return {
latitude,
longitude,
accuracy: Number(res && res.accuracy),
}
},
pickBetterLocation(current, next) {
if (!next) {
return current
}
if (!current) {
return next
}
const currentAccuracy = Number(current.accuracy)
const nextAccuracy = Number(next.accuracy)
if (!Number.isFinite(currentAccuracy)) {
return next
}
if (!Number.isFinite(nextAccuracy)) {
return current
}
return nextAccuracy < currentAccuracy ? next : current
},
async getBestCurrentLocation() {
let best = null
let lastError = null
for (let index = 0; index < 3; index += 1) {
try {
const sample = this.normalizeLocation(await this.requestHighAccuracyLocation())
best = this.pickBetterLocation(best, sample)
if (sample && Number.isFinite(sample.accuracy) && sample.accuracy <= 30) {
break
}
} catch (error) {
lastError = error
}
if (index < 2) {
await this.sleep(700)
}
}
if (best) {
return best
}
throw lastError || new Error('获取位置失败')
},
async useCurrentLocation(e) {
const pointIndex = e && e.currentTarget ? e.currentTarget.dataset.index : undefined
this.setData({ locating: true })
try {
const location = await this.getBestCurrentLocation()
const accuracyText = this.formatAccuracy(location.accuracy)
const updates = {
locationAccuracyText: accuracyText ? `定位精度约 ${accuracyText}` : '',
}
if (pointIndex !== undefined && pointIndex !== '') {
updates[`form.attendancePoints[${pointIndex}].latitude`] = this.formatCoordinate(location.latitude)
updates[`form.attendancePoints[${pointIndex}].longitude`] = this.formatCoordinate(location.longitude)
} else {
updates['form.attendanceLatitude'] = this.formatCoordinate(location.latitude)
updates['form.attendanceLongitude'] = this.formatCoordinate(location.longitude)
}
this.setData(updates)
if (Number.isFinite(location.accuracy) && location.accuracy > 100) {
wx.showModal({
title: '定位精度偏低',
content: '当前只拿到模糊定位,请在系统和微信里开启精确位置,或手动修正经纬度后保存。',
showCancel: false,
})
} else {
wx.showToast({ title: '已获取当前位置', icon: 'success' })
}
} catch (error) {
const message = error && error.errMsg ? error.errMsg : ''
wx.showToast({
title: message.indexOf('auth deny') >= 0 ? '请允许获取位置' : '获取位置失败',
icon: 'none',
})
} finally {
this.setData({ locating: false })
}
},
resetDefault() {
this.setData({
form: {
...this.data.form,
...defaultScheduleValues(),
attendanceLatitude: this.data.form.attendanceLatitude,
attendanceLongitude: this.data.form.attendanceLongitude,
attendanceRadiusMeters: this.data.form.attendanceRadiusMeters || defaultForm.attendanceRadiusMeters,
autoSubmitHours: this.data.form.autoSubmitHours || defaultForm.autoSubmitHours,
attendancePoints: (this.data.form.attendancePoints || []).map(point => ({
...point,
...defaultScheduleValues(),
})),
},
})
},
validateSchedule(point, name) {
for (let index = 0; index < scheduleFields.length; index += 1) {
const field = scheduleFields[index]
const value = String(point[field] || '').trim()
if (!/^\d{1,2}:\d{2}$/.test(value)) {
wx.showToast({ title: `${name} 作息时间格式错误`, icon: 'none' })
return false
}
const parts = value.split(':')
const hour = Number(parts[0])
const minute = Number(parts[1])
if (!Number.isInteger(hour) || !Number.isInteger(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
wx.showToast({ title: `${name} 作息时间范围错误`, icon: 'none' })
return false
}
}
return true
},
validateAttendancePoints(points) {
const names = new Set()
for (let index = 0; index < points.length; index += 1) {
const point = points[index]
const name = String(point.name || '').trim()
if (!name) {
wx.showToast({ title: `${index + 1}个考勤点缺少名称`, icon: 'none' })
return false
}
if (names.has(name)) {
wx.showToast({ title: `考勤点名称重复:${name}`, icon: 'none' })
return false
}
names.add(name)
const latitudeText = String(point.latitude || '').trim()
const longitudeText = String(point.longitude || '').trim()
if ((latitudeText && !longitudeText) || (!latitudeText && longitudeText)) {
wx.showToast({ title: `${name} 经纬度需要同时填写`, icon: 'none' })
return false
}
if (latitudeText || longitudeText) {
const latitude = Number(latitudeText)
const longitude = Number(longitudeText)
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
wx.showToast({ title: `${name} 纬度范围为-90到90`, icon: 'none' })
return false
}
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
wx.showToast({ title: `${name} 经度范围为-180到180`, icon: 'none' })
return false
}
}
const radius = Number(point.radiusMeters)
if (!Number.isFinite(radius) || radius <= 0) {
wx.showToast({ title: `${name} 请输入有效考勤半径`, icon: 'none' })
return false
}
if (!this.validateSchedule(point, name)) {
return false
}
}
return true
},
async removePoint(e) {
const index = e.currentTarget.dataset.index
const point = this.data.form.attendancePoints[index]
if (!point) {
return
}
if (!point.originalName) {
const nextPoints = this.data.form.attendancePoints.filter((_, itemIndex) => itemIndex !== Number(index))
this.setData({
'form.attendancePoints': nextPoints,
expandedPointKey: this.data.expandedPointKey === point.localKey ? '' : this.data.expandedPointKey,
})
return
}
wx.showModal({
title: '停用考勤点',
content: `确认停用 ${point.name}?已有历史业务数据会保留。`,
confirmText: '停用',
confirmColor: '#d92d20',
success: async res => {
if (!res.confirm) {
return
}
try {
await api.deleteAttendancePoint(point.originalName)
wx.showToast({ title: '已停用', icon: 'success' })
await this.load()
} catch (error) {
wx.showToast({ title: error.message || '停用失败', icon: 'none' })
}
},
})
},
async save() {
const radius = Number(this.data.form.attendanceRadiusMeters)
if (!Number.isFinite(radius) || radius <= 0) {
wx.showToast({ title: '请输入有效考勤半径', icon: 'none' })
return
}
const latitudeText = String(this.data.form.attendanceLatitude || '').trim()
const longitudeText = String(this.data.form.attendanceLongitude || '').trim()
const autoSubmitHours = Number(this.data.form.autoSubmitHours)
if (!Number.isFinite(autoSubmitHours) || autoSubmitHours < 1 || autoSubmitHours > 168) {
wx.showToast({ title: '系统自动提交时长需在1到168小时之间', icon: 'none' })
return
}
if ((latitudeText && !longitudeText) || (!latitudeText && longitudeText)) {
wx.showToast({ title: '经纬度需要同时填写', icon: 'none' })
return
}
if (latitudeText || longitudeText) {
const latitude = Number(latitudeText)
const longitude = Number(longitudeText)
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
wx.showToast({ title: '纬度范围为-90到90', icon: 'none' })
return
}
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
wx.showToast({ title: '经度范围为-180到180', icon: 'none' })
return
}
}
this.setData({ saving: true })
wx.showLoading({ title: '保存中' })
try {
const points = this.data.form.attendancePoints || []
if (!points.length) {
wx.showToast({ title: '请至少维护一个考勤点', icon: 'none' })
return
}
if (!this.validateAttendancePoints(points)) {
return
}
const primaryPoint = points[0] || {}
const schedulePayload = {
...this.data.form,
...this.buildPointSchedule(primaryPoint, this.data.form),
attendanceLatitude: primaryPoint.latitude || this.data.form.attendanceLatitude,
attendanceLongitude: primaryPoint.longitude || this.data.form.attendanceLongitude,
attendanceRadiusMeters: primaryPoint.radiusMeters || this.data.form.attendanceRadiusMeters,
attendancePoints: points.map(point => ({
...point,
name: String(point.name || '').trim(),
originalName: point.originalName || point.name,
})),
}
const schedule = await api.saveWorkScheduleSettings(schedulePayload)
wx.showToast({ title: '已保存', icon: 'success' })
this.setData({
form: this.buildForm(schedule),
updatedAtText: schedule.updatedAtText || '',
})
await this.load()
} catch (error) {
wx.showToast({ title: error.message || '保存失败', icon: 'none' })
} finally {
this.setData({ saving: false })
wx.hideLoading()
}
},
})