1285 lines
30 KiB
Vue
1285 lines
30 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view v-if="loading" class="empty-tip text-gray text-center padding">加载中...</view>
|
||
|
||
<view v-else-if="!checkItemList.length" class="empty-tip text-gray text-center padding">
|
||
暂无检查项
|
||
</view>
|
||
|
||
<template v-else>
|
||
<!-- 吸顶进度导航条:紧贴页面内容区顶部,滚动时始终可见 -->
|
||
<view class="float-nav">
|
||
<view class="float-nav-body">
|
||
<view class="float-nav-main">
|
||
<view class="progress-text">
|
||
<text class="current-index">共 {{ checkItemList.length }} 项检查</text>
|
||
<text class="total-count">,已提交 {{ submittedCount }} 项</text>
|
||
<text class="total-count">,待提交 {{ pendingSubmitCount }} 项</text>
|
||
<text class="total-count">,待检查 {{ uncheckedCount }} 项</text>
|
||
</view>
|
||
<view class="progress-line">
|
||
<view class="progress-inner" :style="{ width: progressPercent + '%' }"></view>
|
||
</view>
|
||
</view>
|
||
<view class="sheet-nav-btn" @click="openCheckSheet">
|
||
<up-icon name="grid" size="22" color="#2667E9"></up-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="page-content padding">
|
||
<view
|
||
v-if="showRestoreBanner"
|
||
class="draft-banner margin-bottom"
|
||
>
|
||
<view class="draft-banner-left">
|
||
<text class="cuIcon-info draft-banner-icon"></text>
|
||
<text>已自动恢复您上次未提交的检查内容</text>
|
||
</view>
|
||
<text class="draft-banner-action" @click="clearDraft(true)">清空草稿</text>
|
||
</view>
|
||
|
||
<!-- <view class="sheet-header bg-white radius padding margin-bottom">
|
||
<view class="text-bold">{{ sheetTitle }}</view>
|
||
<view class="text-gray text-sm margin-top-xs" v-if="industryName">
|
||
行业类型:{{ industryName }}
|
||
</view>
|
||
</view> -->
|
||
|
||
<view
|
||
v-for="(item, index) in checkItemList"
|
||
:key="'check-item-' + index"
|
||
:id="'check-item-' + index"
|
||
class="question-card padding bg-white radius margin-bottom"
|
||
:class="{ 'question-card--submitted': isItemSubmitted(item) }"
|
||
>
|
||
<view class="question-header-row">
|
||
<view class="question-index">第 {{ index + 1 }} 项</view>
|
||
<view v-if="isItemSubmitted(item)" class="submitted-tag">已提交</view>
|
||
<view v-else-if="isItemPendingSubmit(index)" class="pending-tag">待提交</view>
|
||
</view>
|
||
<view class="text-bold margin-top-xs">{{ item.name }}</view>
|
||
<view class="margin-top-sm">
|
||
<rich-text :nodes="item.point || ''"></rich-text>
|
||
</view>
|
||
|
||
<!-- 已提交:只读展示 -->
|
||
<block v-if="isItemSubmitted(item)">
|
||
<view class="answer-panel margin-top">
|
||
<view class="readonly-row">
|
||
<text class="readonly-label">检查结果</text>
|
||
<view :class="['result-tag', getResultTagClass(item.result)]">
|
||
{{ getResultLabel(item.result) }}
|
||
</view>
|
||
</view>
|
||
<view v-if="item.remark" class="readonly-row">
|
||
<text class="readonly-label">备注</text>
|
||
<text class="readonly-value">{{ item.remark }}</text>
|
||
</view>
|
||
<view v-if="item.result === 2" class="hazard-summary">
|
||
<view v-if="item.hazardName" class="readonly-row readonly-row--stack">
|
||
<text class="readonly-label">隐患标题</text>
|
||
<text class="readonly-value">{{ item.hazardName }}</text>
|
||
</view>
|
||
<view v-if="item.source" class="readonly-row readonly-row--stack">
|
||
<text class="readonly-label">检查形式</text>
|
||
<text class="readonly-value">{{ item.source }}</text>
|
||
</view>
|
||
<view v-if="item.hazardAddress" class="readonly-row readonly-row--stack">
|
||
<text class="readonly-label">隐患位置</text>
|
||
<text class="readonly-value">{{ item.hazardAddress }}</text>
|
||
</view>
|
||
<view v-if="item.hazardDesc" class="readonly-row readonly-row--stack">
|
||
<text class="readonly-label">隐患描述</text>
|
||
<text class="readonly-value">{{ item.hazardDesc }}</text>
|
||
</view>
|
||
</view>
|
||
<button
|
||
v-if="item.result === 2 && item.hazardId"
|
||
class="view-hazard-btn"
|
||
@click="viewHazardDetail(item)"
|
||
>
|
||
查看隐患
|
||
</button>
|
||
</view>
|
||
</block>
|
||
|
||
<!-- 未提交:可编辑 -->
|
||
<block v-else>
|
||
<view class="margin-top">
|
||
<view class="text-gray margin-bottom-xs">检查结果</view>
|
||
<u-radio-group
|
||
v-model="answerList[index].resultLabel"
|
||
placement="row"
|
||
@change="(label) => onResultChange(index, label)"
|
||
>
|
||
<u-radio
|
||
v-for="(opt, optIndex) in resultOptions"
|
||
:key="optIndex"
|
||
:customStyle="{ marginBottom: '8px', marginRight: '24rpx' }"
|
||
:label="opt.name"
|
||
:name="opt.name"
|
||
></u-radio>
|
||
</u-radio-group>
|
||
</view>
|
||
|
||
<view v-if="answerList[index].resultLabel === '异常'" class="hazard-section margin-top">
|
||
<view class="hazard-tip">
|
||
<text class="cuIcon-warn text-yellow margin-right-xs"></text>
|
||
<text class="text-orange">检查结果为异常,需填写隐患信息</text>
|
||
</view>
|
||
|
||
<view
|
||
v-if="!hasHazardData(index)"
|
||
class="hazard-btn"
|
||
@click="openHazardPopup(index)"
|
||
>
|
||
<text class="text-blue">填写隐患信息</text>
|
||
</view>
|
||
|
||
<view v-else class="hazard-card">
|
||
<view class="card-header">
|
||
<view class="text-bold text-black">{{ answerList[index].hazardPayload.formData.title }}</view>
|
||
<view
|
||
class="level-tag"
|
||
:class="{
|
||
'level-minor': answerList[index].hazardPayload.formData.level === 0,
|
||
'level-normal': answerList[index].hazardPayload.formData.level === 1,
|
||
'level-major': answerList[index].hazardPayload.formData.level === 2
|
||
}"
|
||
>
|
||
{{ LEVEL_OPTIONS[answerList[index].hazardPayload.formData.level]?.title }}
|
||
</view>
|
||
</view>
|
||
<view class="card-body">
|
||
<view class="info-row">
|
||
<text class="text-gray">检查形式:</text>
|
||
<text>{{ SOURCE_OPTIONS[answerList[index].hazardPayload.formData.source]?.title || '-' }}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="text-gray">隐患位置:</text>
|
||
<text>{{ answerList[index].hazardPayload.address || '-' }}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="text-gray">隐患描述:</text>
|
||
<text class="description-text">{{ answerList[index].hazardPayload.formData.description || '-' }}</text>
|
||
</view>
|
||
<view class="info-row" v-if="answerList[index].hazardPayload.fileList.length > 0">
|
||
<text class="text-gray">附件:</text>
|
||
<text>{{ answerList[index].hazardPayload.fileList.length }}个文件</text>
|
||
</view>
|
||
</view>
|
||
<view class="card-footer">
|
||
<button class="btn-edit" @click="openHazardPopup(index)">修改</button>
|
||
<button class="btn-clear" @click="clearHazard(index)">清除</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="margin-top">
|
||
<up-textarea
|
||
v-model="answerList[index].remark"
|
||
placeholder="请输入备注信息"
|
||
></up-textarea>
|
||
</view>
|
||
</block>
|
||
</view>
|
||
|
||
<button
|
||
v-if="pendingSubmitCount > 0"
|
||
class="bg-blue round margin-top-xl submit-btn"
|
||
@click="handleSubmitAll"
|
||
>
|
||
提交已填写项({{ pendingSubmitCount }}项)
|
||
</button>
|
||
</view>
|
||
</template>
|
||
|
||
<HazardFormPopup
|
||
v-if="activeHazardIndex >= 0"
|
||
ref="hazardFormRef"
|
||
v-model:show="showHazardPopup"
|
||
v-model="editingHazardPayload"
|
||
title="填写隐患信息"
|
||
mode="collect"
|
||
body-height="60vh"
|
||
:extra-params="hazardFormExtraParams"
|
||
@confirm="onHazardConfirm"
|
||
/>
|
||
|
||
<u-popup
|
||
:show="showCheckSheet"
|
||
mode="bottom"
|
||
round="20"
|
||
:safe-area-inset-bottom="true"
|
||
@close="closeCheckSheet"
|
||
>
|
||
<view class="check-sheet-popup">
|
||
<view class="check-sheet-header">
|
||
<view class="check-sheet-title">检查项导航</view>
|
||
<view class="check-sheet-subtitle">
|
||
共 {{ checkItemList.length }} 个检查项
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view class="check-sheet-grid-wrap" scroll-y>
|
||
<view class="check-sheet-grid">
|
||
<view
|
||
v-for="(item, index) in checkItemList"
|
||
:key="'sheet-nav-' + index"
|
||
class="check-sheet-cell"
|
||
:class="getSheetCellClass(index)"
|
||
@click="scrollToItem(index)"
|
||
>
|
||
{{ index + 1 }}
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view class="check-sheet-footer">
|
||
<view class="check-sheet-legend">
|
||
<view class="legend-item">
|
||
<view class="legend-dot legend-dot--submitted"></view>
|
||
<text>已提交 {{ submittedCount }}</text>
|
||
</view>
|
||
<view class="legend-item">
|
||
<view class="legend-dot legend-dot--pending"></view>
|
||
<text>待提交 {{ pendingSubmitCount }}</text>
|
||
</view>
|
||
<view class="legend-item">
|
||
<view class="legend-dot legend-dot--todo"></view>
|
||
<text>待检查 {{ uncheckedCount }}</text>
|
||
</view>
|
||
</view>
|
||
<button
|
||
v-if="pendingSubmitCount > 0"
|
||
class="check-sheet-submit-btn"
|
||
@click="handleSheetSubmit"
|
||
>
|
||
立即提交
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, nextTick } from 'vue';
|
||
import { onLoad } from '@dcloudio/uni-app';
|
||
import { getAllTask, submitAllTask } from '@/request/api.js';
|
||
import HazardFormPopup from '@/components/hazard/HazardFormPopup.vue';
|
||
import { buildDraftKey, DRAFT_NS, loadDraftWithFallback, saveDraftToStorage, removeDraft } from '@/utils/draftCache.js';
|
||
import { useDraftCache } from '@/utils/useDraftCache.js';
|
||
import {
|
||
LEVEL_OPTIONS,
|
||
SOURCE_OPTIONS,
|
||
createEmptyHazardPayload,
|
||
buildHiddenDangerParams,
|
||
hasValidHazardPayload
|
||
} from '@/components/hazard';
|
||
|
||
const oneTableId = ref('');
|
||
const tableId = ref('');
|
||
const taskDate = ref('');
|
||
const planName = ref('');
|
||
const loading = ref(true);
|
||
const checkItemList = ref([]);
|
||
const answerList = ref([]);
|
||
const sessionSubmittedTaskIds = ref(new Set());
|
||
|
||
const showHazardPopup = ref(false);
|
||
const showCheckSheet = ref(false);
|
||
const activeHazardIndex = ref(-1);
|
||
const hazardFormRef = ref(null);
|
||
const editingHazardPayload = ref(createEmptyHazardPayload());
|
||
|
||
const resultOptions = [
|
||
{ name: '正常', value: 1 },
|
||
{ name: '异常', value: 2 },
|
||
{ name: '不涉及', value: 3 }
|
||
];
|
||
|
||
const TASK_STATUS_SUBMITTED = 2;
|
||
|
||
const syncSubmittedTaskIds = (list) => {
|
||
const next = new Set(sessionSubmittedTaskIds.value);
|
||
(list || []).forEach((item) => {
|
||
if (item?.status === TASK_STATUS_SUBMITTED && item.taskId != null) {
|
||
next.add(item.taskId);
|
||
}
|
||
});
|
||
sessionSubmittedTaskIds.value = next;
|
||
};
|
||
|
||
const isItemSubmitted = (item) => {
|
||
if (!item) return false;
|
||
if (item.status === TASK_STATUS_SUBMITTED) return true;
|
||
if (item.taskId != null && sessionSubmittedTaskIds.value.has(item.taskId)) return true;
|
||
return false;
|
||
};
|
||
|
||
const getResultLabel = (result) => {
|
||
const opt = resultOptions.find((o) => o.value === result);
|
||
return opt?.name || '-';
|
||
};
|
||
|
||
const getResultTagClass = (result) => {
|
||
if (result === 1) return 'result-tag--normal';
|
||
if (result === 2) return 'result-tag--abnormal';
|
||
if (result === 3) return 'result-tag--na';
|
||
return '';
|
||
};
|
||
|
||
const viewHazardDetail = (item) => {
|
||
if (!item?.hazardId) {
|
||
uni.showToast({ title: '暂无隐患信息', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.navigateTo({
|
||
url: `/pages/hiddendanger/view?hazardId=${item.hazardId}`
|
||
});
|
||
};
|
||
|
||
const sheetTitle = computed(() => {
|
||
return planName.value || checkItemList.value[0]?.itemName || '检查表';
|
||
});
|
||
|
||
const industryName = computed(() => checkItemList.value[0]?.industryName || '');
|
||
|
||
const createEmptyAnswer = () => ({
|
||
resultLabel: '',
|
||
result: null,
|
||
remark: '',
|
||
hazardPayload: createEmptyHazardPayload()
|
||
});
|
||
|
||
const normalizeSheetAnswer = (answer) => {
|
||
if (!answer) return createEmptyAnswer();
|
||
|
||
const hazardPayload = answer.hazardPayload || createEmptyHazardPayload();
|
||
const empty = createEmptyHazardPayload();
|
||
|
||
return {
|
||
resultLabel: answer.resultLabel || '',
|
||
result: answer.result ?? null,
|
||
remark: answer.remark || '',
|
||
hazardPayload: {
|
||
formData: {
|
||
...empty.formData,
|
||
...(hazardPayload.formData || {})
|
||
},
|
||
address: hazardPayload.address || '',
|
||
lng: hazardPayload.lng || 0,
|
||
lat: hazardPayload.lat || 0,
|
||
areaId: hazardPayload.areaId || '',
|
||
areaName: hazardPayload.areaName || '',
|
||
fileList: Array.isArray(hazardPayload.fileList) ? [...hazardPayload.fileList] : [],
|
||
tagOptions: Array.isArray(hazardPayload.tagOptions) ? [...hazardPayload.tagOptions] : []
|
||
}
|
||
};
|
||
};
|
||
|
||
const syncAnswerFields = (answer) => {
|
||
const normalized = normalizeSheetAnswer(answer);
|
||
if (!normalized.resultLabel && normalized.result != null) {
|
||
const opt = resultOptions.find((o) => o.value === normalized.result);
|
||
if (opt) normalized.resultLabel = opt.name;
|
||
} else if (normalized.resultLabel && normalized.result == null) {
|
||
const opt = resultOptions.find((o) => o.name === normalized.resultLabel);
|
||
if (opt) normalized.result = opt.value;
|
||
}
|
||
return normalized;
|
||
};
|
||
|
||
const buildAnswerFromTask = (item) => {
|
||
const answer = createEmptyAnswer();
|
||
if (!isItemSubmitted(item)) {
|
||
return answer;
|
||
}
|
||
|
||
answer.result = item.result;
|
||
answer.remark = item.remark || '';
|
||
const opt = resultOptions.find((o) => o.value === item.result);
|
||
if (opt) {
|
||
answer.resultLabel = opt.name;
|
||
}
|
||
|
||
if (item.result === 2 && item.hazardName) {
|
||
const sourceIndex = SOURCE_OPTIONS.findIndex((o) => o.title === item.source);
|
||
answer.hazardPayload = {
|
||
...createEmptyHazardPayload(),
|
||
formData: {
|
||
...createEmptyHazardPayload().formData,
|
||
title: item.hazardName || '',
|
||
description: item.hazardDesc || '',
|
||
source: sourceIndex >= 0 ? sourceIndex : 0
|
||
},
|
||
address: item.hazardAddress || ''
|
||
};
|
||
}
|
||
|
||
return syncAnswerFields(answer);
|
||
};
|
||
|
||
const isAnswerFilled = (answer) => {
|
||
if (!answer) return false;
|
||
return !!(answer.resultLabel || answer.result != null);
|
||
};
|
||
|
||
const hasAnswerDraftContent = (answer) => {
|
||
if (!answer) return false;
|
||
const normalized = normalizeSheetAnswer(answer);
|
||
const hazard = normalized.hazardPayload;
|
||
return !!(
|
||
normalized.resultLabel ||
|
||
normalized.result != null ||
|
||
normalized.remark ||
|
||
hazard.formData?.title ||
|
||
hazard.formData?.description ||
|
||
(hazard.fileList && hazard.fileList.length > 0)
|
||
);
|
||
};
|
||
|
||
const buildDraftAnswers = () => {
|
||
return checkItemList.value
|
||
.map((item, index) => {
|
||
if (isItemSubmitted(item)) return null;
|
||
const answer = answerList.value[index] || createEmptyAnswer();
|
||
if (!hasAnswerDraftContent(answer)) return null;
|
||
return {
|
||
taskId: item.taskId,
|
||
...JSON.parse(JSON.stringify(answer))
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
};
|
||
|
||
const pruneDraftSubmitted = (taskIds = []) => {
|
||
const key = buildDraftKey(DRAFT_NS.INSPECTION_SHEET, oneTableId.value, taskDate.value);
|
||
const { data } = loadDraftWithFallback(key);
|
||
if (!data?.answers?.length) return;
|
||
|
||
const submittedSet = new Set(taskIds);
|
||
const remainingAnswers = data.answers.filter(
|
||
(answer) => answer?.taskId == null || !submittedSet.has(answer.taskId)
|
||
);
|
||
|
||
if (!remainingAnswers.length) {
|
||
removeDraft(key);
|
||
return;
|
||
}
|
||
|
||
saveDraftToStorage(key, {
|
||
...data,
|
||
answers: remainingAnswers
|
||
});
|
||
};
|
||
|
||
const isItemPendingSubmit = (index) => {
|
||
const item = checkItemList.value[index];
|
||
return !isItemSubmitted(item) && isAnswerFilled(answerList.value[index]);
|
||
};
|
||
|
||
const submittedCount = computed(() => {
|
||
return checkItemList.value.filter(isItemSubmitted).length;
|
||
});
|
||
|
||
const pendingSubmitCount = computed(() => {
|
||
return checkItemList.value.filter((item, index) => isItemPendingSubmit(index)).length;
|
||
});
|
||
|
||
const uncheckedCount = computed(() => {
|
||
return Math.max(checkItemList.value.length - submittedCount.value - pendingSubmitCount.value, 0);
|
||
});
|
||
|
||
const getSheetCellClass = (index) => {
|
||
const item = checkItemList.value[index];
|
||
if (isItemSubmitted(item)) return 'check-sheet-cell--submitted';
|
||
if (isItemPendingSubmit(index)) return 'check-sheet-cell--pending';
|
||
return '';
|
||
};
|
||
|
||
const progressPercent = computed(() => {
|
||
if (!checkItemList.value.length) return 0;
|
||
return Math.round((submittedCount.value / checkItemList.value.length) * 100);
|
||
});
|
||
|
||
const sheetDraftHasContent = (data) => {
|
||
if (!data?.answers?.length) return false;
|
||
|
||
return data.answers.some((answer) => hasAnswerDraftContent(answer));
|
||
};
|
||
|
||
const {
|
||
showRestoreBanner,
|
||
clear: clearDraft,
|
||
restore: restoreDraft,
|
||
bindAutoSave
|
||
} = useDraftCache({
|
||
getKey: () => buildDraftKey(DRAFT_NS.INSPECTION_SHEET, oneTableId.value, taskDate.value),
|
||
getPayload: () => ({
|
||
oneTableId: oneTableId.value,
|
||
taskDate: taskDate.value,
|
||
planName: planName.value,
|
||
answers: buildDraftAnswers()
|
||
}),
|
||
hasContent: sheetDraftHasContent,
|
||
applyPayload: (data) => {
|
||
if (!data?.answers?.length || !checkItemList.value.length) return;
|
||
|
||
// 优先按数组下标恢复,已提交项以接口数据为准
|
||
answerList.value = checkItemList.value.map((item, index) => {
|
||
if (isItemSubmitted(item)) {
|
||
return buildAnswerFromTask(item);
|
||
}
|
||
|
||
const savedByIndex = data.answers[index];
|
||
if (savedByIndex && sheetDraftHasContent({ answers: [savedByIndex] })) {
|
||
return syncAnswerFields(savedByIndex);
|
||
}
|
||
if (item.taskId != null && item.taskId !== '') {
|
||
const savedByTaskId = data.answers.find((a) => a.taskId === item.taskId);
|
||
if (savedByTaskId) {
|
||
return syncAnswerFields(savedByTaskId);
|
||
}
|
||
}
|
||
return createEmptyAnswer();
|
||
});
|
||
|
||
if (data.planName && !planName.value) {
|
||
planName.value = data.planName;
|
||
}
|
||
},
|
||
clearForm: () => {
|
||
answerList.value = checkItemList.value.map((item) => (
|
||
isItemSubmitted(item) ? buildAnswerFromTask(item) : createEmptyAnswer()
|
||
));
|
||
},
|
||
canSave: () => !!oneTableId.value && !!taskDate.value && checkItemList.value.length > 0,
|
||
requireInitialized: true
|
||
});
|
||
|
||
bindAutoSave(() => answerList.value);
|
||
|
||
const onResultChange = (index, label) => {
|
||
const answer = answerList.value[index];
|
||
if (!answer) return;
|
||
answer.resultLabel = label;
|
||
const selected = resultOptions.find((opt) => opt.name === label);
|
||
answer.result = selected ? selected.value : null;
|
||
if (label !== '异常') {
|
||
answer.hazardPayload = createEmptyHazardPayload();
|
||
}
|
||
};
|
||
|
||
const hazardFormExtraParams = computed(() => {
|
||
const index = activeHazardIndex.value;
|
||
const taskId = index >= 0 ? checkItemList.value[index]?.taskId : '';
|
||
return {
|
||
locationDraftKey: `hazard_location_${oneTableId.value}_${taskDate.value}_${taskId || index}`
|
||
};
|
||
});
|
||
|
||
const hasHazardData = (index) => hasValidHazardPayload(answerList.value[index]?.hazardPayload);
|
||
|
||
const openHazardPopup = (index) => {
|
||
activeHazardIndex.value = index;
|
||
editingHazardPayload.value = JSON.parse(JSON.stringify(
|
||
answerList.value[index]?.hazardPayload ?? createEmptyHazardPayload()
|
||
));
|
||
showHazardPopup.value = true;
|
||
};
|
||
|
||
const closeHazardPopup = () => {
|
||
showHazardPopup.value = false;
|
||
activeHazardIndex.value = -1;
|
||
editingHazardPayload.value = createEmptyHazardPayload();
|
||
};
|
||
|
||
const onHazardConfirm = (payload) => {
|
||
const index = activeHazardIndex.value;
|
||
const data = payload || editingHazardPayload.value;
|
||
if (index >= 0 && data && answerList.value[index]) {
|
||
answerList.value[index].hazardPayload = JSON.parse(JSON.stringify(data));
|
||
}
|
||
closeHazardPopup();
|
||
};
|
||
|
||
const clearHazard = (index) => {
|
||
if (answerList.value[index]) {
|
||
answerList.value[index].hazardPayload = createEmptyHazardPayload();
|
||
}
|
||
};
|
||
|
||
const openCheckSheet = () => {
|
||
showCheckSheet.value = true;
|
||
};
|
||
|
||
const closeCheckSheet = () => {
|
||
showCheckSheet.value = false;
|
||
};
|
||
|
||
const handleSheetSubmit = () => {
|
||
closeCheckSheet();
|
||
handleSubmitAll();
|
||
};
|
||
|
||
const scrollToItem = (index) => {
|
||
closeCheckSheet();
|
||
nextTick(() => {
|
||
uni.pageScrollTo({
|
||
selector: `#check-item-${index}`,
|
||
duration: 300,
|
||
offsetTop: -80
|
||
});
|
||
});
|
||
};
|
||
|
||
const getFilledAnswerIndices = () => {
|
||
const indices = [];
|
||
for (let i = 0; i < checkItemList.value.length; i++) {
|
||
if (!isItemSubmitted(checkItemList.value[i]) && isAnswerFilled(answerList.value[i])) {
|
||
indices.push(i);
|
||
}
|
||
}
|
||
return indices;
|
||
};
|
||
|
||
const validateFilledAnswers = () => {
|
||
const indices = getFilledAnswerIndices();
|
||
if (!indices.length) {
|
||
uni.showToast({ title: '请先填写待提交的检查项', icon: 'none' });
|
||
return null;
|
||
}
|
||
|
||
for (const index of indices) {
|
||
const answer = answerList.value[index];
|
||
if (answer.resultLabel === '异常' && !hasValidHazardPayload(answer.hazardPayload)) {
|
||
uni.showToast({ title: `第 ${index + 1} 项为异常,请填写隐患信息`, icon: 'none' });
|
||
return null;
|
||
}
|
||
}
|
||
|
||
return indices;
|
||
};
|
||
|
||
const buildSubmitPayload = (filledIndices) => {
|
||
return filledIndices
|
||
.filter((index) => !isItemSubmitted(checkItemList.value[index]))
|
||
.map((index) => {
|
||
const item = checkItemList.value[index];
|
||
const answer = syncAnswerFields(answerList.value[index]);
|
||
const payload = {
|
||
taskId: item.taskId,
|
||
result: answer.result,
|
||
remark: answer.remark || ''
|
||
};
|
||
|
||
if (answer.resultLabel === '异常' && hasValidHazardPayload(answer.hazardPayload)) {
|
||
payload.hazard = buildHiddenDangerParams(answer.hazardPayload, {
|
||
taskId: item.taskId,
|
||
checkPointId: item.checkPointId
|
||
});
|
||
}
|
||
|
||
return payload;
|
||
});
|
||
};
|
||
|
||
const goToListPage = () => {
|
||
uni.navigateBack();
|
||
};
|
||
|
||
const handleSubmitAll = async () => {
|
||
const filledIndices = validateFilledAnswers();
|
||
if (!filledIndices) return;
|
||
|
||
const submitList = buildSubmitPayload(filledIndices);
|
||
if (!submitList.length) {
|
||
uni.showToast({ title: '没有可提交的新检查项', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
try {
|
||
uni.showLoading({ title: '提交中...' });
|
||
const res = await submitAllTask(submitList);
|
||
uni.hideLoading();
|
||
|
||
if (res.code === 0) {
|
||
const nextSubmitted = new Set(sessionSubmittedTaskIds.value);
|
||
submitList.forEach((item) => {
|
||
if (item.taskId != null) {
|
||
nextSubmitted.add(item.taskId);
|
||
}
|
||
});
|
||
sessionSubmittedTaskIds.value = nextSubmitted;
|
||
pruneDraftSubmitted(submitList.map((item) => item.taskId));
|
||
uni.showToast({
|
||
title: `已成功提交 ${submitList.length} 项`,
|
||
icon: 'success'
|
||
});
|
||
setTimeout(() => {
|
||
goToListPage();
|
||
}, 1500);
|
||
} else {
|
||
uni.showToast({ title: res.msg || '提交失败', icon: 'none' });
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('提交失败:', error);
|
||
uni.showToast({ title: '提交失败', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
const fetchDetail = async () => {
|
||
if (!oneTableId.value || !taskDate.value) {
|
||
loading.value = false;
|
||
uni.showToast({ title: '缺少任务参数', icon: 'none' });
|
||
return;
|
||
}
|
||
loading.value = true;
|
||
try {
|
||
const res = await getAllTask(oneTableId.value, taskDate.value);
|
||
if (res.code === 0) {
|
||
const list = Array.isArray(res.data) ? res.data : (res.data?.records || []);
|
||
syncSubmittedTaskIds(list);
|
||
checkItemList.value = list;
|
||
answerList.value = list.map((item) => buildAnswerFromTask(item));
|
||
restoreDraft();
|
||
} else {
|
||
uni.showToast({ title: res.msg || '获取检查项失败', icon: 'none' });
|
||
}
|
||
} catch (error) {
|
||
console.error('获取检查项失败:', error);
|
||
uni.showToast({ title: '获取检查项失败', icon: 'none' });
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
onLoad((options) => {
|
||
if (options.oneTableId) {
|
||
oneTableId.value = options.oneTableId;
|
||
} else if (options.id) {
|
||
oneTableId.value = options.id;
|
||
}
|
||
if (options.taskDate) {
|
||
taskDate.value = decodeURIComponent(options.taskDate).split(' ')[0];
|
||
}
|
||
if (options.name) {
|
||
planName.value = decodeURIComponent(options.name);
|
||
}
|
||
if (options.tableId) {
|
||
tableId.value = options.tableId;
|
||
}
|
||
fetchDetail();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background: #EBF2FC;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
.page-content {
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
.empty-tip {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.sheet-header {
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.float-nav {
|
||
position: sticky;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 100;
|
||
background: #fff;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||
padding: 20rpx 24rpx;
|
||
}
|
||
|
||
.float-nav-body {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.float-nav-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.sheet-nav-btn {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
background: #eef3ff;
|
||
border-radius: 16rpx;
|
||
}
|
||
|
||
.question-card {
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.question-card--submitted {
|
||
background: #f8faf8;
|
||
border: 1rpx solid #e8f5e9;
|
||
}
|
||
|
||
.question-header-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.question-index {
|
||
display: inline-block;
|
||
padding: 4rpx 16rpx;
|
||
font-size: 24rpx;
|
||
color: #2667E9;
|
||
background: #EEF3FF;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.submitted-tag {
|
||
padding: 4rpx 16rpx;
|
||
font-size: 22rpx;
|
||
color: #52c41a;
|
||
background: #f6ffed;
|
||
border: 1rpx solid #b7eb8f;
|
||
border-radius: 8rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.pending-tag {
|
||
padding: 4rpx 16rpx;
|
||
font-size: 22rpx;
|
||
color: #2667e9;
|
||
background: #eef3ff;
|
||
border: 1rpx solid #adc6ff;
|
||
border-radius: 8rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.readonly-block,
|
||
.answer-panel {
|
||
padding: 20rpx;
|
||
background: #f7f8fa;
|
||
border: 1rpx solid #e8e8e8;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.readonly-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
margin-bottom: 12rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.readonly-row--stack {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.hazard-summary {
|
||
margin-top: 16rpx;
|
||
padding-top: 16rpx;
|
||
border-top: 1rpx dashed #e0e0e0;
|
||
}
|
||
|
||
.readonly-label {
|
||
flex-shrink: 0;
|
||
color: #999;
|
||
min-width: 120rpx;
|
||
}
|
||
|
||
.readonly-value {
|
||
flex: 1;
|
||
color: #333;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.result-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 4rpx 16rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
line-height: 1.4;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.result-tag--normal {
|
||
background: #f6ffed;
|
||
border: 1rpx solid #b7eb8f;
|
||
color: #52c41a;
|
||
}
|
||
|
||
.result-tag--abnormal {
|
||
background: #fff1f0;
|
||
border: 1rpx solid #ffa39e;
|
||
color: #f5222d;
|
||
}
|
||
|
||
.result-tag--na {
|
||
background: #f5f5f5;
|
||
border: 1rpx solid #d9d9d9;
|
||
color: #8c8c8c;
|
||
}
|
||
|
||
.view-hazard-btn {
|
||
margin-top: 20rpx;
|
||
width: 100%;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
font-size: 28rpx;
|
||
color: #2667e9;
|
||
background: #fff;
|
||
border: 1rpx solid #adc6ff;
|
||
border-radius: 12rpx;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
.progress-text {
|
||
display: flex;
|
||
align-items: baseline;
|
||
margin-bottom: 16rpx;
|
||
|
||
.current-index {
|
||
font-size: 26rpx;
|
||
font-weight: bold;
|
||
color: #2667E9;
|
||
}
|
||
|
||
.total-count {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.progress-line {
|
||
height: 12rpx;
|
||
background: #E5E5E5;
|
||
border-radius: 6rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-inner {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #2667E9, #5B9BFF);
|
||
border-radius: 6rpx;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.hazard-section {
|
||
border-top: 1rpx solid #eee;
|
||
padding-top: 20rpx;
|
||
}
|
||
|
||
.hazard-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16rpx 20rpx;
|
||
background: #FFF7E6;
|
||
border: 1rpx solid #FFE7BA;
|
||
border-radius: 8rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
.text-orange {
|
||
color: #FA8C16;
|
||
font-size: 26rpx;
|
||
}
|
||
}
|
||
|
||
.hazard-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 88rpx;
|
||
border: 2rpx dashed #2667E9;
|
||
border-radius: 12rpx;
|
||
background: #F5F9FF;
|
||
|
||
.text-blue {
|
||
color: #2667E9;
|
||
font-size: 28rpx;
|
||
}
|
||
}
|
||
|
||
.hazard-card {
|
||
background: #F5F9FF;
|
||
border: 1rpx solid #D6E4FF;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx;
|
||
border-bottom: 1rpx solid #E8E8E8;
|
||
background: #fff;
|
||
}
|
||
|
||
.card-body {
|
||
padding: 20rpx;
|
||
|
||
.info-row {
|
||
display: flex;
|
||
margin-bottom: 12rpx;
|
||
font-size: 26rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.text-gray {
|
||
flex-shrink: 0;
|
||
color: #999;
|
||
}
|
||
|
||
.description-text {
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-footer {
|
||
display: flex;
|
||
border-top: 1rpx solid #E8E8E8;
|
||
background: #fff;
|
||
|
||
button {
|
||
flex: 1;
|
||
height: 80rpx;
|
||
line-height: 80rpx;
|
||
font-size: 28rpx;
|
||
border-radius: 0;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
.btn-edit {
|
||
background: #fff;
|
||
color: #2667E9;
|
||
border-right: 1rpx solid #E8E8E8;
|
||
}
|
||
|
||
.btn-clear {
|
||
background: #fff;
|
||
color: #F56C6C;
|
||
}
|
||
}
|
||
}
|
||
|
||
.level-tag {
|
||
padding: 4rpx 16rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.level-minor {
|
||
background: #F6FFED;
|
||
border: 2rpx solid #B7EB8F;
|
||
color: #52C41A;
|
||
}
|
||
|
||
.level-normal {
|
||
background: #FFF7E6;
|
||
border: 2rpx solid #FFD591;
|
||
color: #FA8C16;
|
||
}
|
||
|
||
.level-major {
|
||
background: #FFF1F0;
|
||
border: 2rpx solid #FFA39E;
|
||
color: #F5222D;
|
||
}
|
||
|
||
.submit-btn {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.draft-banner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx 24rpx;
|
||
font-size: 24rpx;
|
||
color: #fa8c16;
|
||
background: #fff7eb;
|
||
border: 1rpx solid #ffe4cc;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.draft-banner-left {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.draft-banner-icon {
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.draft-banner-action {
|
||
flex-shrink: 0;
|
||
margin-left: 16rpx;
|
||
color: #2667e9;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.check-sheet-popup {
|
||
padding: 32rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
|
||
background: #fff;
|
||
}
|
||
|
||
.check-sheet-header {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.check-sheet-title {
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.check-sheet-subtitle {
|
||
margin-top: 8rpx;
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.check-sheet-grid-wrap {
|
||
max-height: 52vh;
|
||
}
|
||
|
||
.check-sheet-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 20rpx;
|
||
padding-bottom: 8rpx;
|
||
}
|
||
|
||
.check-sheet-cell {
|
||
width: calc((100% - 80rpx) / 5);
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
background: #fff;
|
||
border: 2rpx solid #e5e5e5;
|
||
border-radius: 12rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.check-sheet-cell--pending {
|
||
color: #fff;
|
||
background: #2667e9;
|
||
border-color: #2667e9;
|
||
}
|
||
|
||
.check-sheet-cell--submitted {
|
||
color: #52c41a;
|
||
background: #f6ffed;
|
||
border-color: #b7eb8f;
|
||
}
|
||
|
||
.check-sheet-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 24rpx;
|
||
margin-top: 28rpx;
|
||
padding-top: 24rpx;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
.check-sheet-legend {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 32rpx;
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.check-sheet-submit-btn {
|
||
flex-shrink: 0;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
padding: 0 36rpx;
|
||
margin: 0;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
color: #fff;
|
||
background: linear-gradient(135deg, #4facfe 0%, #2668ea 100%);
|
||
border-radius: 36rpx;
|
||
border: none;
|
||
box-shadow: 0 4rpx 16rpx rgba(38, 104, 234, 0.3);
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.legend-dot {
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
border-radius: 8rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.legend-dot--submitted {
|
||
background: #f6ffed;
|
||
border: 2rpx solid #b7eb8f;
|
||
}
|
||
|
||
.legend-dot--pending {
|
||
background: #2667e9;
|
||
border: 2rpx solid #2667e9;
|
||
}
|
||
|
||
.legend-dot--todo {
|
||
background: #fff;
|
||
border: 2rpx solid #e5e5e5;
|
||
}
|
||
</style>
|