480 lines
16 KiB
JavaScript
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()
|
|
}
|
|
},
|
|
})
|