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() } }, })