|
|
@@ -0,0 +1,483 @@
|
|
|
+<template>
|
|
|
+ <view class="questionnaire-task">
|
|
|
+ <view v-if="configLoading" class="questionnaire-task__page-state">
|
|
|
+ <wd-loading color="#3b9bed" />
|
|
|
+ <text>任务配置加载中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-else-if="configError" class="questionnaire-task__page-state">
|
|
|
+ <text>{{ configError }}</text>
|
|
|
+ <wd-button size="small" type="primary" @click="loadTaskConfig">重新加载</wd-button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <view class="questionnaire-task__panel">
|
|
|
+ <wd-cell
|
|
|
+ title-width="200rpx"
|
|
|
+ :title="packageFieldLabel"
|
|
|
+ :placeholder="`请选择${packageFieldLabel}`"
|
|
|
+ :value="selectedPackageLabel"
|
|
|
+ required
|
|
|
+ is-link
|
|
|
+ @click="openPackagePicker"
|
|
|
+ />
|
|
|
+
|
|
|
+ <wd-cell v-if="questionnaireLoading" title-width="200rpx" title="问卷" required>
|
|
|
+ <view class="questionnaire-task__loading">
|
|
|
+ <wd-loading size="34rpx" color="#3b9bed" />
|
|
|
+ <text>加载中...</text>
|
|
|
+ </view>
|
|
|
+ </wd-cell>
|
|
|
+
|
|
|
+ <wd-cell
|
|
|
+ v-else-if="questionnaireOptions.length"
|
|
|
+ title-width="200rpx"
|
|
|
+ title="问卷"
|
|
|
+ placeholder="请选择问卷"
|
|
|
+ :value="selectedQuestionnaireLabel"
|
|
|
+ required
|
|
|
+ is-link
|
|
|
+ @click="openQuestionnairePicker"
|
|
|
+ />
|
|
|
+
|
|
|
+ <wd-cell v-else title-width="200rpx" title="问卷" required>
|
|
|
+ <wd-input
|
|
|
+ v-model="manualQuestionnaireUrl"
|
|
|
+ class="questionnaire-task__input"
|
|
|
+ placeholder="请输入问卷地址"
|
|
|
+ clearable
|
|
|
+ align-right
|
|
|
+ custom-style="--wot-input-padding: 0; padding: 0;"
|
|
|
+ @input="clearQrCode"
|
|
|
+ />
|
|
|
+ <template #suffix>
|
|
|
+ <wd-button
|
|
|
+ size="mini"
|
|
|
+ type="success"
|
|
|
+ custom-style="min-width: 88rpx; margin-left: 12rpx; padding: 0 18rpx;"
|
|
|
+ :disabled="qrCodeLoading"
|
|
|
+ @click.stop="generateManualQrCode"
|
|
|
+ >
|
|
|
+ 生成
|
|
|
+ </wd-button>
|
|
|
+ </template>
|
|
|
+ </wd-cell>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="qrCodeImageUrl" class="questionnaire-task__qrcode">
|
|
|
+ <image
|
|
|
+ class="questionnaire-task__image"
|
|
|
+ :src="qrCodeImageUrl"
|
|
|
+ mode="aspectFit"
|
|
|
+ :show-menu-by-longpress="true"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <canvas
|
|
|
+ canvas-id="questionnaireQrCanvas"
|
|
|
+ class="questionnaire-task__canvas"
|
|
|
+ :style="canvasStyle"
|
|
|
+ />
|
|
|
+
|
|
|
+ <wd-picker
|
|
|
+ v-model="selectedPackagePickerValue"
|
|
|
+ v-model:visible="packagePickerVisible"
|
|
|
+ :columns="packageOptions"
|
|
|
+ @confirm="handlePackageConfirm"
|
|
|
+ />
|
|
|
+
|
|
|
+ <wd-picker
|
|
|
+ v-model="selectedQuestionnairePickerValue"
|
|
|
+ v-model:visible="questionnairePickerVisible"
|
|
|
+ :columns="questionnaireOptions"
|
|
|
+ @confirm="handleQuestionnaireConfirm"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, getCurrentInstance, nextTick, onMounted, ref } from 'vue'
|
|
|
+
|
|
|
+import UQRCode from 'uqrcodejs'
|
|
|
+
|
|
|
+import {
|
|
|
+ getQuestionnaireListApi,
|
|
|
+ getQuestionnaireTaskContentConfigApi,
|
|
|
+} from '@/services/modules/task/taskFrom'
|
|
|
+import type {
|
|
|
+ QuestionnaireListItem,
|
|
|
+ TaskFieldConfigItem,
|
|
|
+} from '@/services/modules/task/taskFrom/type'
|
|
|
+
|
|
|
+import { useUserStore } from '@/stores/modules/user'
|
|
|
+
|
|
|
+import { hideGlobalLoading, showGlobalLoading } from '@/utils/loading'
|
|
|
+
|
|
|
+import {
|
|
|
+ buildQuestionnaireQrContent,
|
|
|
+ resolveQuestionnaireEnvironment,
|
|
|
+} from '../utils/questionnaire'
|
|
|
+
|
|
|
+interface PickerOption {
|
|
|
+ label: string
|
|
|
+ value: string
|
|
|
+}
|
|
|
+
|
|
|
+interface PickerConfirmEvent {
|
|
|
+ selectedItems: PickerOption[]
|
|
|
+}
|
|
|
+
|
|
|
+const QR_CODE_CANVAS_ID = 'questionnaireQrCanvas'
|
|
|
+const QR_CODE_SIZE = 280
|
|
|
+const TASK_TYPE_ID = '801'
|
|
|
+
|
|
|
+const instance = getCurrentInstance()
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+const configLoading = ref(false)
|
|
|
+const configError = ref('')
|
|
|
+const packageField = ref<TaskFieldConfigItem | null>(null)
|
|
|
+const packageOptions = ref<PickerOption[]>([])
|
|
|
+const packagePickerVisible = ref(false)
|
|
|
+const selectedPackagePickerValue = ref<string[]>([])
|
|
|
+const selectedPackageLabel = ref('')
|
|
|
+const selectedPackageId = ref('')
|
|
|
+
|
|
|
+const questionnaireList = ref<QuestionnaireListItem[]>([])
|
|
|
+const questionnaireLoading = ref(false)
|
|
|
+const questionnairePickerVisible = ref(false)
|
|
|
+const selectedQuestionnairePickerValue = ref<string[]>([])
|
|
|
+const selectedQuestionnaireLabel = ref('')
|
|
|
+const manualQuestionnaireUrl = ref('')
|
|
|
+
|
|
|
+const qrCodeImageUrl = ref('')
|
|
|
+const qrCodeLoading = ref(false)
|
|
|
+
|
|
|
+let questionnaireRequestVersion = 0
|
|
|
+let qrCodeGenerationVersion = 0
|
|
|
+let lastQrContent = ''
|
|
|
+
|
|
|
+const packageFieldLabel = computed(() => packageField.value?.taskFiledValue || '关联积分包')
|
|
|
+
|
|
|
+const questionnaireOptions = computed<PickerOption[]>(() => {
|
|
|
+ return questionnaireList.value.map((item) => ({
|
|
|
+ label: item.label,
|
|
|
+ value: item.url,
|
|
|
+ }))
|
|
|
+})
|
|
|
+
|
|
|
+const canvasStyle = {
|
|
|
+ width: `${QR_CODE_SIZE}px`,
|
|
|
+ height: `${QR_CODE_SIZE}px`,
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ void loadTaskConfig()
|
|
|
+})
|
|
|
+
|
|
|
+const loadTaskConfig = async () => {
|
|
|
+ if (configLoading.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ configLoading.value = true
|
|
|
+ configError.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await getQuestionnaireTaskContentConfigApi()
|
|
|
+ const taskConfig = response.data[TASK_TYPE_ID]
|
|
|
+ const scorePackageField = taskConfig.config.find(
|
|
|
+ (field) => field.alias === 'scorePackage' || field.taskFiledKey === 'temp32'
|
|
|
+ )
|
|
|
+
|
|
|
+ if (!scorePackageField) {
|
|
|
+ throw new Error('801 任务配置中缺少关联积分包字段')
|
|
|
+ }
|
|
|
+
|
|
|
+ const dictGroupName = scorePackageField.dictGroupName?.trim() || ''
|
|
|
+ const dictItems = dictGroupName ? (taskConfig.dict[dictGroupName] ?? []) : []
|
|
|
+
|
|
|
+ packageField.value = scorePackageField
|
|
|
+ packageOptions.value = dictItems
|
|
|
+ .map((item) => ({
|
|
|
+ label: String(item.label ?? '').trim(),
|
|
|
+ value: String(item.value ?? '').trim(),
|
|
|
+ }))
|
|
|
+ .filter((item) => item.label && item.value)
|
|
|
+
|
|
|
+ if (!packageOptions.value.length) {
|
|
|
+ throw new Error('暂无可用的关联积分包')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[QuestionnaireTask] 加载 801 配置失败', error)
|
|
|
+ configError.value = error instanceof Error ? error.message : '任务配置加载失败'
|
|
|
+ } finally {
|
|
|
+ configLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openPackagePicker = () => {
|
|
|
+ if (!packageOptions.value.length) {
|
|
|
+ uni.showToast({ title: configError.value || '暂无可选积分包', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ packagePickerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handlePackageConfirm = (event: PickerConfirmEvent) => {
|
|
|
+ const selectedItem = event.selectedItems[0]
|
|
|
+
|
|
|
+ if (!selectedItem || selectedItem.value === selectedPackageId.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedPackagePickerValue.value = [selectedItem.value]
|
|
|
+ selectedPackageLabel.value = selectedItem.label
|
|
|
+ selectedPackageId.value = selectedItem.value
|
|
|
+
|
|
|
+ resetQuestionnaire()
|
|
|
+ void loadQuestionnaireList(selectedItem.value)
|
|
|
+}
|
|
|
+
|
|
|
+const resetQuestionnaire = () => {
|
|
|
+ questionnaireRequestVersion += 1
|
|
|
+ questionnaireList.value = []
|
|
|
+ questionnaireLoading.value = false
|
|
|
+ questionnairePickerVisible.value = false
|
|
|
+ selectedQuestionnairePickerValue.value = []
|
|
|
+ selectedQuestionnaireLabel.value = ''
|
|
|
+ manualQuestionnaireUrl.value = ''
|
|
|
+ clearQrCode()
|
|
|
+}
|
|
|
+
|
|
|
+const loadQuestionnaireList = async (packageId: string) => {
|
|
|
+ const requestVersion = ++questionnaireRequestVersion
|
|
|
+
|
|
|
+ questionnaireLoading.value = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await getQuestionnaireListApi(packageId)
|
|
|
+
|
|
|
+ if (requestVersion !== questionnaireRequestVersion) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ questionnaireList.value = Array.isArray(response.data)
|
|
|
+ ? response.data.filter((item) => item.label?.trim() && item.url?.trim())
|
|
|
+ : []
|
|
|
+ } catch (error) {
|
|
|
+ if (requestVersion !== questionnaireRequestVersion) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ questionnaireList.value = []
|
|
|
+ console.error('[QuestionnaireTask] 获取问卷列表失败', error)
|
|
|
+ uni.showToast({
|
|
|
+ title: '问卷列表加载失败,可手动输入问卷地址',
|
|
|
+ icon: 'none',
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ if (requestVersion === questionnaireRequestVersion) {
|
|
|
+ questionnaireLoading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openQuestionnairePicker = () => {
|
|
|
+ if (!selectedPackageId.value) {
|
|
|
+ uni.showToast({ title: '请先选择关联积分包', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ questionnairePickerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const handleQuestionnaireConfirm = (event: PickerConfirmEvent) => {
|
|
|
+ const selectedItem = event.selectedItems[0]
|
|
|
+
|
|
|
+ if (!selectedItem) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedQuestionnairePickerValue.value = [selectedItem.value]
|
|
|
+ selectedQuestionnaireLabel.value = selectedItem.label
|
|
|
+
|
|
|
+ void generateQrCode(selectedItem.value, true)
|
|
|
+}
|
|
|
+
|
|
|
+const generateManualQrCode = () => {
|
|
|
+ if (qrCodeLoading.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const questionnaireUrl = manualQuestionnaireUrl.value.trim()
|
|
|
+
|
|
|
+ if (!selectedPackageId.value) {
|
|
|
+ uni.showToast({ title: '请选择关联积分包', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!questionnaireUrl) {
|
|
|
+ uni.showToast({ title: '请输入问卷地址', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ void generateQrCode(questionnaireUrl, false)
|
|
|
+}
|
|
|
+
|
|
|
+const clearQrCode = () => {
|
|
|
+ qrCodeGenerationVersion += 1
|
|
|
+ qrCodeImageUrl.value = ''
|
|
|
+ lastQrContent = ''
|
|
|
+}
|
|
|
+
|
|
|
+const generateQrCode = async (questionnaireUrl: string, usesQuestionnaireList: boolean) => {
|
|
|
+ const packageId = selectedPackageId.value
|
|
|
+
|
|
|
+ if (!packageId || qrCodeLoading.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const environment = resolveQuestionnaireEnvironment(String(import.meta.env.VITE_BASE_API ?? ''))
|
|
|
+ const qrContent = buildQuestionnaireQrContent({
|
|
|
+ questionnaireUrl,
|
|
|
+ packageId,
|
|
|
+ environment,
|
|
|
+ phone: userStore.currentUserInfo?.phone,
|
|
|
+ usesQuestionnaireList,
|
|
|
+ })
|
|
|
+
|
|
|
+ if (qrContent === lastQrContent && qrCodeImageUrl.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const generationVersion = ++qrCodeGenerationVersion
|
|
|
+
|
|
|
+ qrCodeImageUrl.value = ''
|
|
|
+ qrCodeLoading.value = true
|
|
|
+ showGlobalLoading('二维码生成中')
|
|
|
+
|
|
|
+ try {
|
|
|
+ await nextTick()
|
|
|
+ await waitForLoadingPaint()
|
|
|
+
|
|
|
+ const canvasContext = uni.createCanvasContext(
|
|
|
+ QR_CODE_CANVAS_ID,
|
|
|
+ instance?.proxy as unknown as WechatMiniprogram.Component.TrivialInstance
|
|
|
+ )
|
|
|
+ const qrCode = new UQRCode()
|
|
|
+
|
|
|
+ qrCode.data = qrContent
|
|
|
+ qrCode.size = QR_CODE_SIZE
|
|
|
+ qrCode.margin = 12
|
|
|
+ qrCode.errorCorrectLevel = UQRCode.errorCorrectLevel.M
|
|
|
+ qrCode.backgroundColor = '#ffffff'
|
|
|
+ qrCode.foregroundColor = '#000000'
|
|
|
+ qrCode.canvasContext = canvasContext
|
|
|
+ qrCode.make()
|
|
|
+
|
|
|
+ await qrCode.drawCanvas()
|
|
|
+
|
|
|
+ if (generationVersion !== qrCodeGenerationVersion) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ qrCodeImageUrl.value = await exportQrCodeImage()
|
|
|
+ lastQrContent = qrContent
|
|
|
+ } catch (error) {
|
|
|
+ if (generationVersion !== qrCodeGenerationVersion) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ console.error('[QuestionnaireTask] 生成二维码失败', error)
|
|
|
+ uni.showToast({ title: '二维码生成失败,请重试', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ qrCodeLoading.value = false
|
|
|
+ hideGlobalLoading()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const waitForLoadingPaint = (): Promise<void> => {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ setTimeout(resolve, 60)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const exportQrCodeImage = (): Promise<string> => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.canvasToTempFilePath(
|
|
|
+ {
|
|
|
+ canvasId: QR_CODE_CANVAS_ID,
|
|
|
+ width: QR_CODE_SIZE,
|
|
|
+ height: QR_CODE_SIZE,
|
|
|
+ destWidth: QR_CODE_SIZE,
|
|
|
+ destHeight: QR_CODE_SIZE,
|
|
|
+ fileType: 'png',
|
|
|
+ quality: 1,
|
|
|
+ success: (result) => resolve(result.tempFilePath),
|
|
|
+ fail: reject,
|
|
|
+ },
|
|
|
+ instance?.proxy as unknown as WechatMiniprogram.Component.TrivialInstance
|
|
|
+ )
|
|
|
+ })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.questionnaire-task {
|
|
|
+ min-height: 100vh;
|
|
|
+ padding-top: 20rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.questionnaire-task__page-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 24rpx;
|
|
|
+ min-height: 360rpx;
|
|
|
+ color: #8a8f99;
|
|
|
+ font-size: 28rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.questionnaire-task__panel {
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.questionnaire-task__loading {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 12rpx;
|
|
|
+ width: 100%;
|
|
|
+ color: #8a8f99;
|
|
|
+ font-size: 28rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.questionnaire-task__input {
|
|
|
+ width: 100%;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.questionnaire-task__qrcode {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 36rpx 0;
|
|
|
+}
|
|
|
+
|
|
|
+.questionnaire-task__image {
|
|
|
+ display: block;
|
|
|
+ width: 360rpx;
|
|
|
+ height: 360rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.questionnaire-task__canvas {
|
|
|
+ position: fixed;
|
|
|
+ top: -9999px;
|
|
|
+ left: -9999px;
|
|
|
+}
|
|
|
+</style>
|