feat: 添加动态时间提示和自动校正功能

- 在"最早可提前"下方动态显示最早可签到时间
  - 在"最晚可延后"下方动态显示最晚可签到时间(跨天显示"次日"前缀)
  - 输入值变化时自动校正到临界值,防止时间重叠
  - 调整表单布局支持换行显示提示信息
This commit is contained in:
kuaifan 2026-01-07 03:33:33 +00:00
parent 395fc155ce
commit 4e99e398d6
3 changed files with 90 additions and 25 deletions

View File

@ -391,8 +391,8 @@ class UserBot extends AbstractModel
$errorTime = false;
if (!$targetDate) {
$displayDelay = date("H:i", $todayTimeDelay % 86400);
$nextDay = ($todayTimeDelay > strtotime("{$nowDate} 23:59:59")) ? "(次日)" : "";
$errorTime = "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-{$nextDay}{$displayDelay}";
$nextDay = ($todayTimeDelay > strtotime("{$nowDate} 23:59:59")) ? "(+1)" : "";
$errorTime = "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-{$displayDelay}{$nextDay}";
}
//
$macs = explode(",", $mac);
@ -485,7 +485,7 @@ class UserBot extends AbstractModel
$typeContent = $type == "up" ? "上班" : "下班";
if (Cache::get($cacheKey) === "yes") {
if ($alreadyTip) {
$dateHint = ($displayDate != $nowDate) ? "{$displayDate}" : "今日";
$dateHint = ($displayDate != $nowDate) ? "({$displayDate}) " : "今日";
$text = "{$dateHint}{$typeContent}打卡,无需重复打卡。";
$text .= $checkin['remark'] ? " ({$checkin['remark']})": "";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
@ -500,7 +500,7 @@ class UserBot extends AbstractModel
$hi = date("H:i");
$remark = $checkin['remark'] ? " ({$checkin['remark']})": "";
$subcontent = $getJokeSoup($type, $checkin['userid']);
$dateInfo = ($displayDate != $nowDate) ? "(记录归属 {$displayDate}" : "";
$dateInfo = ($displayDate != $nowDate) ? " ({$displayDate})" : "";
$title = "{$typeContent}打卡成功,打卡时间: {$hi}{$remark}{$dateInfo}";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',

View File

@ -29,15 +29,17 @@
<Form @submit.native.prevent class="block-setting-advance">
<FormItem :label="$L('最早可提前')" prop="advance">
<div class="input-number-box">
<InputNumber v-model="formData.advance" :min="0" :step="1"/>
<InputNumber v-model="formData.advance" :min="0" :step="1" @on-change="onAdvanceBlur"/>
<label>{{ $L('分钟') }}</label>
</div>
<div v-if="earliestCheckinTime" class="form-tip">{{ earliestCheckinTime }}</div>
</FormItem>
<FormItem :label="$L('最晚可延后')" prop="delay">
<div class="input-number-box">
<InputNumber v-model="formData.delay" :min="0" :step="1"/>
<InputNumber v-model="formData.delay" :min="0" :step="1" @on-change="onDelayBlur"/>
<label>{{ $L('分钟') }}</label>
</div>
<div v-if="latestCheckinTime" class="form-tip">{{ latestCheckinTime }}</div>
</FormItem>
<div class="form-tip">{{$L('签到前后时间收到消息通知')}}</div>
<FormItem :label="$L('签到打卡提醒')" prop="remindin">
@ -405,31 +407,54 @@ export default {
computed: {
...mapState(['formOptions']),
earliestCheckinTime() {
const times = this.formData.time;
if (!times || times.length < 1 || !times[0]) return '';
const advance = parseInt(this.formData.advance) || 0;
if (advance <= 0) return '';
const startMinutes = this.timeToMinutes(times[0]);
let earliestMinutes = startMinutes - advance;
//
let prefix = '';
if (earliestMinutes < 0) {
earliestMinutes += 24 * 60;
prefix = '(' + this.$L('前日') + ') ';
}
const hours = Math.floor(earliestMinutes / 60);
const mins = earliestMinutes % 60;
return prefix + String(hours).padStart(2, '0') + ':' + String(mins).padStart(2, '0');
},
latestCheckinTime() {
const times = this.formData.time;
if (!times || times.length < 2 || !times[1]) return '';
const delay = parseInt(this.formData.delay) || 0;
if (delay <= 0) return '';
const endMinutes = this.timeToMinutes(times[1]);
let latestMinutes = endMinutes + delay;
// 24
let prefix = '';
if (latestMinutes >= 24 * 60) {
latestMinutes -= 24 * 60;
prefix = '(' + this.$L('次日') + ') ';
}
const hours = Math.floor(latestMinutes / 60);
const mins = latestMinutes % 60;
return prefix + String(hours).padStart(2, '0') + ':' + String(mins).padStart(2, '0');
},
},
methods: {
submitForm() {
this.$refs.formData.validate((valid) => {
if (valid) {
//
if (this.formData.open === 'open') {
const times = this.formData.time;
if (times && times.length >= 2) {
const startMinutes = this.timeToMinutes(times[0]);
const endMinutes = this.timeToMinutes(times[1]);
let shiftDuration = endMinutes - startMinutes;
if (shiftDuration <= 0) shiftDuration += 24 * 60;
const advance = parseInt(this.formData.advance) || 120;
const delay = parseInt(this.formData.delay) || 120;
const maxAllowed = 24 * 60 - shiftDuration;
if (advance + delay >= maxAllowed) {
$A.modalError('提前和延后时间设置存在重叠,最大提前+延后时间不能超过 ' + (maxAllowed - 1) + ' 分钟', {language: false});
return;
}
}
}
this.systemSetting(true);
}
})
@ -441,6 +466,44 @@ export default {
return parseInt(parts[0]) * 60 + parseInt(parts[1]);
},
getMaxAllowed() {
const times = this.formData.time;
if (!times || times.length < 2) return null;
const startMinutes = this.timeToMinutes(times[0]);
const endMinutes = this.timeToMinutes(times[1]);
let shiftDuration = endMinutes - startMinutes;
if (shiftDuration <= 0) shiftDuration += 24 * 60;
return 24 * 60 - shiftDuration;
},
onAdvanceBlur() {
const maxAllowed = this.getMaxAllowed();
if (maxAllowed === null) return;
const delay = parseInt(this.formData.delay) || 0;
const maxAdvance = maxAllowed - delay - 1;
if (maxAdvance < 0) {
this.formData.advance = 0;
} else if (this.formData.advance > maxAdvance) {
this.formData.advance = maxAdvance;
}
},
onDelayBlur() {
const maxAllowed = this.getMaxAllowed();
if (maxAllowed === null) return;
const advance = parseInt(this.formData.advance) || 0;
const maxDelay = maxAllowed - advance - 1;
if (maxDelay < 0) {
this.formData.delay = 0;
} else if (this.formData.delay > maxDelay) {
this.formData.delay = maxDelay;
}
},
resetForm() {
this.formData = $A.cloneJSON(this.formDatum_bak);
},

View File

@ -900,6 +900,8 @@ body.window-portrait {
.ivu-form-item-content {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
}
}