|
|
@@ -282,6 +282,10 @@
|
|
|
{{ t }}
|
|
|
</el-tag>
|
|
|
<el-button size="small" type="primary" plain @click="openAddTimeSlot">添加时间段</el-button>
|
|
|
+ <div v-if="timeSlotList.length > 0" class="time-slot-hint">
|
|
|
+ <el-icon><InfoFilled /></el-icon>
|
|
|
+ <span>扫码窗口已自动同步:<b>{{ form.scanStartTime || '-' }}</b> 起 <b>{{ formatDuration(form.scanIntervalSec) }}</b> 内有效(覆盖全部巡检时段)</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div v-else-if="form.cycle === '每周'">
|
|
|
<el-checkbox-group v-model="weekDayList">
|
|
|
@@ -546,7 +550,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup name="InspectionTask" lang="ts">
|
|
|
-import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue';
|
|
|
+import { ref, reactive, toRefs, onMounted, getCurrentInstance, watch } from 'vue';
|
|
|
import {
|
|
|
listInspectionTask,
|
|
|
getInspectionTask,
|
|
|
@@ -572,7 +576,7 @@ import type { UserVO } from '@/api/system/user/types';
|
|
|
import { listDept } from '@/api/system/dept';
|
|
|
import { FormInstance } from 'element-plus';
|
|
|
import type { ComponentInternalInstance } from 'vue';
|
|
|
-import { Calendar, Check, Warning, Clock, Location } from '@element-plus/icons-vue';
|
|
|
+import { Calendar, Check, Warning, Clock, Location, InfoFilled } from '@element-plus/icons-vue';
|
|
|
import MapPickerDialog from '@/components/MapPickerDialog/index.vue';
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
@@ -851,6 +855,71 @@ const removeTimeSlot = (idx: number) => {
|
|
|
timeSlotList.value.splice(idx, 1);
|
|
|
};
|
|
|
|
|
|
+// ============ 扫码时间自动同步(从 timeSlotList 推导 scanStartTime + scanIntervalSec) ============
|
|
|
+/**
|
|
|
+ * 解析 "HH:mm-HH:mm" 字符串 → { start: 'HH:mm', end: 'HH:mm' } 或 null
|
|
|
+ * 兼容 "16:10-17:00" / "8:5-9:30"(自动补 0)
|
|
|
+ */
|
|
|
+const parseSlot = (s: string): { start: string; end: string } | null => {
|
|
|
+ if (!s || typeof s !== 'string') return null;
|
|
|
+ const m = s.match(/^\s*(\d{1,2}):(\d{1,2})\s*-\s*(\d{1,2}):(\d{1,2})\s*$/);
|
|
|
+ if (!m) return null;
|
|
|
+ const sh = +m[1], sm = +m[2], eh = +m[3], em = +m[4];
|
|
|
+ if ([sh, sm, eh, em].some((n) => Number.isNaN(n))) return null;
|
|
|
+ if (sh > 23 || sm > 59 || eh > 23 || em > 59) return null;
|
|
|
+ if (timeToSec(`${eh}:${em}`) <= timeToSec(`${sh}:${sm}`)) return null; // 结束必须 > 开始
|
|
|
+ const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
+ return { start: `${pad(sh)}:${pad(sm)}`, end: `${pad(eh)}:${pad(em)}` };
|
|
|
+};
|
|
|
+
|
|
|
+/** "HH:mm" → 当天 0 点起的秒数 */
|
|
|
+const timeToSec = (t: string): number => {
|
|
|
+ const [h, m] = t.split(':').map(Number);
|
|
|
+ return h * 3600 + m * 60;
|
|
|
+};
|
|
|
+
|
|
|
+/** 今天 YYYY-MM-DD(本地时区) */
|
|
|
+const todayStr = (): string => {
|
|
|
+ const d = new Date();
|
|
|
+ const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
|
+};
|
|
|
+
|
|
|
+/** 秒数 → "X 小时 Y 分" / "Y 分 Z 秒" 人类可读 */
|
|
|
+const formatDuration = (sec: number | undefined | null): string => {
|
|
|
+ if (!sec || sec <= 0) return '0 秒';
|
|
|
+ const h = Math.floor(sec / 3600);
|
|
|
+ const m = Math.floor((sec % 3600) / 60);
|
|
|
+ const s = sec % 60;
|
|
|
+ if (h > 0) return `${h} 小时 ${m} 分`;
|
|
|
+ if (m > 0) return s > 0 ? `${m} 分 ${s} 秒` : `${m} 分`;
|
|
|
+ return `${s} 秒`;
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 根据 timeSlotList 自动同步 scanStartTime + scanIntervalSec
|
|
|
+ * - 仅 cycle=每日 时生效(其他周期用 dayOfWeek/dayOfMonth,timeSlot 留空)
|
|
|
+ * - 首个 slot 的开始 = 扫码开启时间
|
|
|
+ * - 末个 slot 的结束 - 首个 slot 的开始 = 扫码窗口长度(秒)
|
|
|
+ * 覆盖整个巡检窗口期,任意 slot 期间扫码都有效
|
|
|
+ * - 没有 slot 时不动,保留默认/手动值
|
|
|
+ */
|
|
|
+const syncScanTime = () => {
|
|
|
+ if (form.value.cycle !== '每日') return;
|
|
|
+ const slots = timeSlotList.value
|
|
|
+ .map(parseSlot)
|
|
|
+ .filter((s): s is { start: string; end: string } => !!s);
|
|
|
+ if (slots.length === 0) return;
|
|
|
+ const first = slots[0];
|
|
|
+ const last = slots[slots.length - 1];
|
|
|
+ form.value.scanStartTime = `${todayStr()} ${first.start}:00`;
|
|
|
+ form.value.scanIntervalSec = Math.max(60, timeToSec(last.end) - timeToSec(first.start));
|
|
|
+};
|
|
|
+
|
|
|
+watch(timeSlotList, () => {
|
|
|
+ syncScanTime();
|
|
|
+}, { deep: true });
|
|
|
+
|
|
|
const addInspectionItem = () => {
|
|
|
form.value.itemList.push({
|
|
|
itemCategory: '',
|
|
|
@@ -1248,4 +1317,17 @@ const loadStores = async () => {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
}
|
|
|
+.time-slot-hint {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ line-height: 1.4;
|
|
|
+ b {
|
|
|
+ color: #67c23a;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|