chore: initialize mini program frontend
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
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()
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "考勤设置"
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<scroll-view
|
||||
class="page safe-bottom"
|
||||
scroll-y
|
||||
type="list"
|
||||
refresher-enabled
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onPullDownRefresh"
|
||||
>
|
||||
<view class="header">
|
||||
<text class="title">考勤设置</text>
|
||||
<text class="subtitle">按考勤点维护作息时间、就餐时间和扫码考勤范围</text>
|
||||
</view>
|
||||
|
||||
<view class="card auto-submit-card">
|
||||
<view class="form-item">
|
||||
<text class="label">系统自动提交时长(小时)</text>
|
||||
<input
|
||||
class="input"
|
||||
type="digit"
|
||||
value="{{form.autoSubmitHours}}"
|
||||
placeholder="默认15小时"
|
||||
bindinput="onAutoSubmitHoursInput"
|
||||
/>
|
||||
<text class="subtitle">超过该时长未下班报工,系统将自动提交待审核报工。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title section-title-action">
|
||||
<text>考勤范围</text>
|
||||
<button class="btn mini primary add-point-btn" bindtap="addAttendancePoint">+</button>
|
||||
</view>
|
||||
<view
|
||||
wx:for="{{form.attendancePoints}}"
|
||||
wx:key="localKey"
|
||||
wx:for-index="pointIndex"
|
||||
wx:for-item="point"
|
||||
class="card location-card {{expandedPointKey === point.localKey ? 'expanded' : 'collapsed'}}"
|
||||
>
|
||||
<view class="location-head" data-key="{{point.localKey}}" bindtap="toggleAttendancePoint">
|
||||
<view class="location-text">
|
||||
<text class="schedule-name">{{point.name || '新考勤点'}}</text>
|
||||
<block wx:if="{{expandedPointKey === point.localKey}}">
|
||||
<text class="subtitle">一个考勤点对应一个分公司,可独立配置作息和扫码范围</text>
|
||||
<text wx:if="{{locationAccuracyText}}" class="location-accuracy">{{locationAccuracyText}}</text>
|
||||
</block>
|
||||
</view>
|
||||
<button
|
||||
wx:if="{{expandedPointKey === point.localKey}}"
|
||||
class="btn primary locate-btn"
|
||||
loading="{{locating}}"
|
||||
data-index="{{pointIndex}}"
|
||||
catchtap="useCurrentLocation"
|
||||
>使用当前位置</button>
|
||||
</view>
|
||||
|
||||
<block wx:if="{{expandedPointKey === point.localKey}}">
|
||||
<view class="form-item">
|
||||
<text class="label">考勤点名称</text>
|
||||
<input
|
||||
class="input"
|
||||
value="{{point.name}}"
|
||||
placeholder="请输入考勤点名称"
|
||||
data-index="{{pointIndex}}"
|
||||
data-field="name"
|
||||
bindinput="onPointInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="point-section-title">作息时间</view>
|
||||
<view class="schedule-panel">
|
||||
<view class="schedule-row">
|
||||
<text class="schedule-name">白班时间段</text>
|
||||
<view class="time-range">
|
||||
<picker mode="time" value="{{point.dayStart}}" data-index="{{pointIndex}}" data-field="dayStart" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.dayStart}}</view>
|
||||
</picker>
|
||||
<text class="range-sep">至</text>
|
||||
<picker mode="time" value="{{point.dayEnd}}" data-index="{{pointIndex}}" data-field="dayEnd" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.dayEnd}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="schedule-row">
|
||||
<text class="schedule-name">夜班时间段</text>
|
||||
<view class="time-range">
|
||||
<picker mode="time" value="{{point.nightStart}}" data-index="{{pointIndex}}" data-field="nightStart" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.nightStart}}</view>
|
||||
</picker>
|
||||
<text class="range-sep">至</text>
|
||||
<picker mode="time" value="{{point.nightEnd}}" data-index="{{pointIndex}}" data-field="nightEnd" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.nightEnd}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="schedule-row">
|
||||
<text class="schedule-name">中饭就餐</text>
|
||||
<view class="time-range">
|
||||
<picker mode="time" value="{{point.lunchStart}}" data-index="{{pointIndex}}" data-field="lunchStart" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.lunchStart}}</view>
|
||||
</picker>
|
||||
<text class="range-sep">至</text>
|
||||
<picker mode="time" value="{{point.lunchEnd}}" data-index="{{pointIndex}}" data-field="lunchEnd" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.lunchEnd}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="schedule-row">
|
||||
<text class="schedule-name">晚饭就餐</text>
|
||||
<view class="time-range">
|
||||
<picker mode="time" value="{{point.dinnerStart}}" data-index="{{pointIndex}}" data-field="dinnerStart" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.dinnerStart}}</view>
|
||||
</picker>
|
||||
<text class="range-sep">至</text>
|
||||
<picker mode="time" value="{{point.dinnerEnd}}" data-index="{{pointIndex}}" data-field="dinnerEnd" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.dinnerEnd}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="schedule-row last">
|
||||
<text class="schedule-name">加班时间段</text>
|
||||
<view class="time-range">
|
||||
<picker mode="time" value="{{point.overtimeStart}}" data-index="{{pointIndex}}" data-field="overtimeStart" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.overtimeStart}}</view>
|
||||
</picker>
|
||||
<text class="range-sep">至</text>
|
||||
<picker mode="time" value="{{point.overtimeEnd}}" data-index="{{pointIndex}}" data-field="overtimeEnd" bindchange="onTimeChange">
|
||||
<view class="picker time-picker">{{point.overtimeEnd}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="point-section-title">考勤范围</view>
|
||||
<view class="location-grid">
|
||||
<view class="location-cell">
|
||||
<text class="label">纬度</text>
|
||||
<input
|
||||
class="input coordinate-input mono"
|
||||
type="digit"
|
||||
value="{{point.latitude}}"
|
||||
placeholder="请输入纬度"
|
||||
data-index="{{pointIndex}}"
|
||||
data-field="latitude"
|
||||
bindinput="onPointInput"
|
||||
/>
|
||||
</view>
|
||||
<view class="location-cell">
|
||||
<text class="label">经度</text>
|
||||
<input
|
||||
class="input coordinate-input mono"
|
||||
type="digit"
|
||||
value="{{point.longitude}}"
|
||||
placeholder="请输入经度"
|
||||
data-index="{{pointIndex}}"
|
||||
data-field="longitude"
|
||||
bindinput="onPointInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item radius-item">
|
||||
<text class="label">考勤范围半径(米)</text>
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
value="{{point.radiusMeters}}"
|
||||
placeholder="请输入米数"
|
||||
data-index="{{pointIndex}}"
|
||||
data-field="radiusMeters"
|
||||
bindinput="onPointInput"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">备注</text>
|
||||
<textarea
|
||||
class="textarea"
|
||||
value="{{point.remark}}"
|
||||
placeholder="可填写分公司或厂区说明"
|
||||
data-index="{{pointIndex}}"
|
||||
data-field="remark"
|
||||
bindinput="onPointInput"
|
||||
></textarea>
|
||||
</view>
|
||||
<button class="btn danger full" data-index="{{pointIndex}}" catchtap="removePoint">停用/移除考勤点</button>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<view class="card save-card">
|
||||
<text wx:if="{{updatedAtText}}" class="subtitle">最近更新 {{updatedAtText}}</text>
|
||||
<view class="button-row">
|
||||
<button class="btn secondary full" bindtap="resetDefault">恢复默认</button>
|
||||
<button class="btn primary full" loading="{{saving}}" bindtap="save">保存设置</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,186 @@
|
||||
.schedule-panel {
|
||||
margin-bottom: 26rpx;
|
||||
padding: 6rpx 20rpx;
|
||||
border: 2rpx solid #d7e1ef;
|
||||
border-radius: 16rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.schedule-row {
|
||||
display: grid;
|
||||
grid-template-columns: 178rpx minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid #e6edf6;
|
||||
}
|
||||
|
||||
.schedule-row.last {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.schedule-name {
|
||||
color: #162232;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 42rpx minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.time-picker {
|
||||
min-height: 74rpx;
|
||||
justify-content: center;
|
||||
padding: 0 10rpx;
|
||||
color: #162232;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.range-sep {
|
||||
color: #6b778c;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-card {
|
||||
padding-top: 22rpx;
|
||||
}
|
||||
|
||||
.auto-submit-card {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.point-section-title {
|
||||
margin: 28rpx 0 16rpx;
|
||||
color: #1f64d8;
|
||||
font-size: 25rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.location-head + .point-section-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.save-card .subtitle {
|
||||
margin-top: 0;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.section-title-action {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section-title-action text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.section-title-action::before {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.add-point-btn {
|
||||
width: 64rpx;
|
||||
min-width: 64rpx;
|
||||
padding: 0;
|
||||
font-size: 40rpx;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.location-card {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.location-card.collapsed {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.location-card.expanded .location-head {
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.location-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.location-card.collapsed .location-head {
|
||||
min-height: 92rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.location-text .schedule-name {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.location-accuracy {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: #1f64d8;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.locate-btn {
|
||||
flex: 0 0 220rpx;
|
||||
margin: 0;
|
||||
padding: 0 14rpx;
|
||||
}
|
||||
|
||||
.location-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.location-cell {
|
||||
min-width: 0;
|
||||
padding: 18rpx;
|
||||
border: 2rpx solid #d1dced;
|
||||
border-radius: 14rpx;
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #eef4fb 100%);
|
||||
box-shadow:
|
||||
0 8rpx 0 rgba(181, 194, 213, 0.68),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.92);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: DIN Alternate, Arial, sans-serif;
|
||||
font-size: 26rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.coordinate-input {
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
padding: 0 14rpx;
|
||||
color: #162232;
|
||||
font-weight: 800;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.radius-item {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user