|
|
@@ -14,7 +14,11 @@
|
|
|
|
|
|
<template v-else-if="taskDetail">
|
|
|
<SignTaskDetail v-if="signTaskDetail" :task-detail="signTaskDetail" :records="signRecords" />
|
|
|
- <ShareTaskDetail v-else-if="shareTaskDetail" :task-detail="shareTaskDetail" />
|
|
|
+ <ShareTaskDetail
|
|
|
+ v-else-if="shareTaskDetail"
|
|
|
+ :task-detail="shareTaskDetail"
|
|
|
+ @qr-state-change="handleQrStateChange"
|
|
|
+ />
|
|
|
<NormalTaskDetail
|
|
|
v-else-if="normalTaskDetail"
|
|
|
:task-detail="normalTaskDetail"
|
|
|
@@ -30,16 +34,17 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { computed, ref } from 'vue'
|
|
|
+import { computed, nextTick, ref } from 'vue'
|
|
|
|
|
|
-import { onLoad } from '@dcloudio/uni-app'
|
|
|
+import { onLoad, onUnload } from '@dcloudio/uni-app'
|
|
|
|
|
|
-import { getTaskInfoByIdApi } from '@/services/modules/task/taskDetail'
|
|
|
+import { getTaskInfoByIdApi, updateScreenShotStatusApi } from '@/services/modules/task/taskDetail'
|
|
|
import type {
|
|
|
ShareTaskDetailResponse,
|
|
|
SignTaskDetailResponse,
|
|
|
TaskDetailResponse,
|
|
|
TaskFormDetailResponse,
|
|
|
+ UpdateScreenShotStatusParams,
|
|
|
} from '@/services/modules/task/taskDetail/type'
|
|
|
import { getTaskContentConfigByTaskTypeIdApi } from '@/services/modules/task/taskFrom'
|
|
|
import type { TaskContentConfigByTaskTypeIdResponse } from '@/services/modules/task/taskFrom/type'
|
|
|
@@ -53,17 +58,46 @@ import SignTaskDetail from './components/SignTaskDetail.vue'
|
|
|
interface PageLoadOptions {
|
|
|
id?: string
|
|
|
taskId?: string
|
|
|
+ exportToken?: string
|
|
|
+}
|
|
|
+
|
|
|
+interface QrExportState {
|
|
|
+ ercodeUrl: string
|
|
|
+ qrLoadOk: boolean
|
|
|
+ qrLoadErr: boolean
|
|
|
+ qrErrorMsg: string
|
|
|
+}
|
|
|
+
|
|
|
+interface ImageReadyState {
|
|
|
+ imgTotal: number
|
|
|
+ imgDone: number
|
|
|
+ imgOk: number
|
|
|
+ imgErr: number
|
|
|
}
|
|
|
|
|
|
const SHARE_TASK_TYPES = new Set(['8', '9', '10', '11'])
|
|
|
const SIGN_TASK_TYPES = new Set(['5', '6', '33'])
|
|
|
+const EXPORT_MAX_WAIT_MS = 18_000
|
|
|
+const EXPORT_POLL_INTERVAL_MS = 120
|
|
|
+const QR_IMAGE_SELECTOR = '.share-detail-qrcode__image'
|
|
|
|
|
|
const taskId = ref('')
|
|
|
+const exportToken = ref('')
|
|
|
const loading = ref(false)
|
|
|
const errorMessage = ref('')
|
|
|
const taskDetail = ref<TaskDetailResponse | null>(null)
|
|
|
const taskContentConfig = ref<TaskContentConfigByTaskTypeIdResponse | null>(null)
|
|
|
const signRecords = ref<VisitSignRecordItem[]>([])
|
|
|
+const qrExportState = ref<QrExportState>({
|
|
|
+ ercodeUrl: '',
|
|
|
+ qrLoadOk: false,
|
|
|
+ qrLoadErr: false,
|
|
|
+ qrErrorMsg: '',
|
|
|
+})
|
|
|
+
|
|
|
+let exportStartTs = 0
|
|
|
+let exportReadyNotified = false
|
|
|
+let exportFlowVersion = 0
|
|
|
|
|
|
const isShareTaskDetail = (detail: TaskDetailResponse): detail is ShareTaskDetailResponse => {
|
|
|
return SHARE_TASK_TYPES.has(String(detail.taskType))
|
|
|
@@ -89,6 +123,112 @@ const normalTaskDetail = computed<TaskFormDetailResponse | null>(() => {
|
|
|
return detail
|
|
|
})
|
|
|
|
|
|
+const handleQrStateChange = (state: QrExportState) => {
|
|
|
+ qrExportState.value = state
|
|
|
+}
|
|
|
+
|
|
|
+const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
|
+
|
|
|
+const waitForPaint = async () => {
|
|
|
+ await nextTick()
|
|
|
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
|
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
|
+}
|
|
|
+
|
|
|
+const getPageRoot = (): Element | null => {
|
|
|
+ return document.querySelector('.task-detail-page')
|
|
|
+}
|
|
|
+
|
|
|
+const getQrNaturalWidth = (): number => {
|
|
|
+ const qrImage = getPageRoot()?.querySelector<HTMLImageElement>(QR_IMAGE_SELECTOR)
|
|
|
+ return qrImage ? qrImage.naturalWidth || 0 : -1
|
|
|
+}
|
|
|
+
|
|
|
+const checkAllImagesOnce = (): ImageReadyState => {
|
|
|
+ const images = Array.from(getPageRoot()?.querySelectorAll<HTMLImageElement>('img') ?? [])
|
|
|
+ let imgDone = 0
|
|
|
+ let imgOk = 0
|
|
|
+ let imgErr = 0
|
|
|
+
|
|
|
+ for (const image of images) {
|
|
|
+ const isQrImage = image.matches(QR_IMAGE_SELECTOR)
|
|
|
+
|
|
|
+ if (image.complete && image.naturalWidth > 0) {
|
|
|
+ imgDone += 1
|
|
|
+ imgOk += 1
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // 二维码必须成功解码;其他图片即使失败也视为加载结束,避免阻塞截图任务。
|
|
|
+ if (!isQrImage && image.complete && image.naturalWidth === 0) {
|
|
|
+ imgDone += 1
|
|
|
+ imgErr += 1
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ imgTotal: images.length,
|
|
|
+ imgDone,
|
|
|
+ imgOk,
|
|
|
+ imgErr,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const mockNotifyExportReady = async (data: UpdateScreenShotStatusParams) => {
|
|
|
+ try {
|
|
|
+ const res = await updateScreenShotStatusApi(data)
|
|
|
+ console.log('[task-detail] mockNotifyExportReady', res)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[task-detail] 截图就绪状态通知失败', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const notifyExportReadyOnce = (ok: boolean, timeout: boolean) => {
|
|
|
+ if (!exportToken.value || exportReadyNotified) return
|
|
|
+
|
|
|
+ exportReadyNotified = true
|
|
|
+
|
|
|
+ const data: UpdateScreenShotStatusParams = {
|
|
|
+ exportToken: exportToken.value,
|
|
|
+ ok: String(ok),
|
|
|
+ timeout: String(timeout),
|
|
|
+ costMs: String(Math.max(0, Date.now() - exportStartTs)),
|
|
|
+ qrLoadOk: String(qrExportState.value.qrLoadOk),
|
|
|
+ qrLoadErr: String(qrExportState.value.qrLoadErr),
|
|
|
+ qrErrorMsg: qrExportState.value.qrErrorMsg,
|
|
|
+ qrNaturalWidth: String(getQrNaturalWidth()),
|
|
|
+ ercodeUrl: qrExportState.value.ercodeUrl,
|
|
|
+ }
|
|
|
+
|
|
|
+ void mockNotifyExportReady(data)
|
|
|
+}
|
|
|
+
|
|
|
+const waitAndNotifyExportReady = async (flowVersion: number) => {
|
|
|
+ const deadline = exportStartTs + EXPORT_MAX_WAIT_MS
|
|
|
+
|
|
|
+ while (flowVersion === exportFlowVersion && Date.now() < deadline) {
|
|
|
+ await waitForPaint()
|
|
|
+
|
|
|
+ const imageState = checkAllImagesOnce()
|
|
|
+ const allImagesDone = imageState.imgTotal === 0 || imageState.imgDone >= imageState.imgTotal
|
|
|
+ const qrReady =
|
|
|
+ !shareTaskDetail.value ||
|
|
|
+ (Boolean(qrExportState.value.ercodeUrl) &&
|
|
|
+ (qrExportState.value.qrLoadOk || getQrNaturalWidth() > 0))
|
|
|
+
|
|
|
+ if (qrReady && allImagesDone) {
|
|
|
+ notifyExportReadyOnce(true, false)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ await sleep(EXPORT_POLL_INTERVAL_MS)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (flowVersion === exportFlowVersion) {
|
|
|
+ notifyExportReadyOnce(false, true)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const loadTaskDetail = async () => {
|
|
|
if (!taskId.value || loading.value) return
|
|
|
|
|
|
@@ -130,15 +270,41 @@ const loadTaskDetail = async () => {
|
|
|
}
|
|
|
|
|
|
onLoad((options: PageLoadOptions = {}) => {
|
|
|
+ exportFlowVersion += 1
|
|
|
+ const flowVersion = exportFlowVersion
|
|
|
+
|
|
|
taskId.value = String(options.taskId ?? options.id ?? '').trim()
|
|
|
+ exportToken.value = String(options.exportToken ?? '').trim()
|
|
|
+ exportStartTs = Date.now()
|
|
|
+ exportReadyNotified = false
|
|
|
+ qrExportState.value = {
|
|
|
+ ercodeUrl: '',
|
|
|
+ qrLoadOk: false,
|
|
|
+ qrLoadErr: false,
|
|
|
+ qrErrorMsg: '',
|
|
|
+ }
|
|
|
|
|
|
if (!taskId.value) {
|
|
|
errorMessage.value = '缺少 taskId'
|
|
|
uni.showToast({ title: errorMessage.value, icon: 'none' })
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ if (exportToken.value) void waitAndNotifyExportReady(flowVersion)
|
|
|
+ // #endif
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- void loadTaskDetail()
|
|
|
+ void (async () => {
|
|
|
+ await loadTaskDetail()
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ if (exportToken.value) await waitAndNotifyExportReady(flowVersion)
|
|
|
+ // #endif
|
|
|
+ })()
|
|
|
+})
|
|
|
+
|
|
|
+onUnload(() => {
|
|
|
+ exportFlowVersion += 1
|
|
|
})
|
|
|
</script>
|
|
|
|