|
|
@@ -0,0 +1,1179 @@
|
|
|
+<template>
|
|
|
+ <view class="visit-status-card">
|
|
|
+ <view class="visit-status-card__main">
|
|
|
+ <view class="visit-status-card__copy">
|
|
|
+ <view class="visit-status-card__meta">
|
|
|
+ <view
|
|
|
+ class="visit-status-card__meta-dot"
|
|
|
+ :class="{
|
|
|
+ 'visit-status-card__meta-dot--success': canCheckIn,
|
|
|
+ 'visit-status-card__meta-dot--error': hasStatusError,
|
|
|
+ }"
|
|
|
+ />
|
|
|
+ <text>定位状态</text>
|
|
|
+ <view
|
|
|
+ class="visit-status-card__badge"
|
|
|
+ :class="{
|
|
|
+ 'visit-status-card__badge--success': canCheckIn,
|
|
|
+ 'visit-status-card__badge--error': hasStatusError,
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ {{ statusView.badge }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="visit-status-card__title">{{ statusView.title }}</view>
|
|
|
+
|
|
|
+ <view v-if="targetName" class="visit-status-card__place">
|
|
|
+ <wd-icon name="location" size="28rpx" color="#2f8cf7" />
|
|
|
+ <view class="visit-status-card__place-copy">
|
|
|
+ <text class="visit-status-card__place-name">{{ targetName }}</text>
|
|
|
+ <text class="visit-status-card__place-address">{{ targetAddress }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="statusView.description" class="visit-status-card__desc">
|
|
|
+ {{ statusView.description }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view
|
|
|
+ class="visit-status-card__action-wrap"
|
|
|
+ :class="{ 'visit-status-card__action-wrap--disabled': !canCheckIn }"
|
|
|
+ @tap="handleCheckInClick"
|
|
|
+ >
|
|
|
+ <view
|
|
|
+ class="visit-status-card__action-ring"
|
|
|
+ :class="{ 'visit-status-card__action-ring--active': canCheckIn }"
|
|
|
+ >
|
|
|
+ <view
|
|
|
+ class="visit-status-card__action"
|
|
|
+ :class="{
|
|
|
+ 'visit-status-card__action--active': canCheckIn,
|
|
|
+ 'visit-status-card__action--countdown': remainingSeconds > 0,
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <text>{{ actionText.primary }}</text>
|
|
|
+ <text v-if="actionText.secondary">{{ actionText.secondary }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="visit-status-card__footer" @tap="emit('open-visit-time')">
|
|
|
+ <view class="visit-status-card__footer-icon">
|
|
|
+ <wd-icon name="time-line" size="30rpx" color="#2f8cf7" />
|
|
|
+ </view>
|
|
|
+ <text class="visit-status-card__footer-title">查看打卡时间</text>
|
|
|
+ <wd-icon name="right" size="28rpx" color="#9aa8b9" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <wd-picker
|
|
|
+ v-model="selectedPackageValues"
|
|
|
+ v-model:visible="packagePickerVisible"
|
|
|
+ title="选择积分包"
|
|
|
+ :columns="packageList"
|
|
|
+ root-portal
|
|
|
+ @confirm="confirmPackageSelection"
|
|
|
+ />
|
|
|
+
|
|
|
+ <wd-popup
|
|
|
+ v-model="templateVisible"
|
|
|
+ position="bottom"
|
|
|
+ root-portal
|
|
|
+ lock-scroll
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ custom-style="border-radius: 32rpx 32rpx 0 0; overflow: hidden;"
|
|
|
+ @close="resetTemplateForm"
|
|
|
+ >
|
|
|
+ <view class="visit-template">
|
|
|
+ <view class="visit-template__header">
|
|
|
+ <view>
|
|
|
+ <view class="visit-template__title">拜访明细</view>
|
|
|
+ <view class="visit-template__subtitle">{{ templateTarget?.name }}</view>
|
|
|
+ </view>
|
|
|
+ <view class="visit-template__close" @click="templateVisible = false">
|
|
|
+ <wd-icon name="close" size="32rpx" color="#8b98a8" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="templateLoading" class="visit-template__loading">正在加载拜访模板</view>
|
|
|
+
|
|
|
+ <scroll-view v-else class="visit-template__scroll" scroll-y>
|
|
|
+ <wd-cell title="拜访时间" :value="templateSignDate" />
|
|
|
+
|
|
|
+ <wd-cell
|
|
|
+ v-if="templateTarget?.type === 'pharmacy'"
|
|
|
+ title="拜访人员"
|
|
|
+ required
|
|
|
+ >
|
|
|
+ <wd-input
|
|
|
+ v-model="templateForm.visitor"
|
|
|
+ no-border
|
|
|
+ align-right
|
|
|
+ placeholder="请输入拜访人员"
|
|
|
+ />
|
|
|
+ </wd-cell>
|
|
|
+
|
|
|
+ <wd-cell title="推广药品" required is-link :value="templateDrugText" @click="openDrugPicker" />
|
|
|
+ <wd-cell
|
|
|
+ title="拜访目的"
|
|
|
+ required
|
|
|
+ is-link
|
|
|
+ :value="templatePurposeText"
|
|
|
+ @click="purposePickerVisible = true"
|
|
|
+ />
|
|
|
+ <wd-cell
|
|
|
+ title="拜访结果"
|
|
|
+ required
|
|
|
+ is-link
|
|
|
+ :value="templateResultText"
|
|
|
+ @click="resultPickerVisible = true"
|
|
|
+ />
|
|
|
+
|
|
|
+ <wd-cell v-if="requiresDepartment" title="拜访科室" required>
|
|
|
+ <wd-input
|
|
|
+ v-model="templateForm.department"
|
|
|
+ no-border
|
|
|
+ align-right
|
|
|
+ placeholder="请输入拜访科室"
|
|
|
+ />
|
|
|
+ </wd-cell>
|
|
|
+
|
|
|
+ <ImgUpload
|
|
|
+ v-if="requiresTemplatePhoto"
|
|
|
+ v-model:value="templateForm.photoUrls"
|
|
|
+ :task-field-config="templatePhotoFieldConfig"
|
|
|
+ />
|
|
|
+
|
|
|
+ <template v-if="templateTarget?.type === 'hospital' && templateType === 'TEMPLATE2'">
|
|
|
+ <view
|
|
|
+ v-for="item in templateEvaluationItems"
|
|
|
+ :key="item.key"
|
|
|
+ class="visit-template__field"
|
|
|
+ >
|
|
|
+ <view class="visit-template__field-label">
|
|
|
+ <text class="visit-template__required">*</text>{{ item.label }}
|
|
|
+ </view>
|
|
|
+ <wd-radio-group
|
|
|
+ v-model="templateForm[item.key]"
|
|
|
+ direction="horizontal"
|
|
|
+ type="button"
|
|
|
+ >
|
|
|
+ <wd-radio value="true">是</wd-radio>
|
|
|
+ <wd-radio value="false">否</wd-radio>
|
|
|
+ </wd-radio-group>
|
|
|
+ </view>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <view class="visit-template__field visit-template__field--vertical">
|
|
|
+ <view class="visit-template__field-label">备注</view>
|
|
|
+ <wd-textarea
|
|
|
+ v-model="templateForm.remark"
|
|
|
+ placeholder="请输入备注"
|
|
|
+ clearable
|
|
|
+ auto-height
|
|
|
+ custom-style="min-height: 180rpx;"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+
|
|
|
+ <view class="visit-template__actions">
|
|
|
+ <button class="visit-template__button visit-template__button--cancel" @click="templateVisible = false">
|
|
|
+ 取消
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="visit-template__button visit-template__button--submit"
|
|
|
+ :disabled="signSubmitting"
|
|
|
+ @click="submitTemplateSign"
|
|
|
+ >
|
|
|
+ {{ signSubmitting ? '提交中' : '提交打卡' }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </wd-popup>
|
|
|
+
|
|
|
+ <wd-select-picker
|
|
|
+ v-model="templateForm.drug"
|
|
|
+ v-model:visible="drugPickerVisible"
|
|
|
+ title="选择推广药品"
|
|
|
+ :columns="drugOptions"
|
|
|
+ root-portal
|
|
|
+ />
|
|
|
+ <wd-select-picker
|
|
|
+ v-model="templateForm.purpose"
|
|
|
+ v-model:visible="purposePickerVisible"
|
|
|
+ title="选择拜访目的"
|
|
|
+ :columns="purposeOptions"
|
|
|
+ root-portal
|
|
|
+ />
|
|
|
+ <wd-picker
|
|
|
+ v-model="templateForm.result"
|
|
|
+ v-model:visible="resultPickerVisible"
|
|
|
+ title="选择拜访结果"
|
|
|
+ :columns="resultOptions"
|
|
|
+ root-portal
|
|
|
+ />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
|
+
|
|
|
+import dayjs from 'dayjs'
|
|
|
+
|
|
|
+import type { LocationResult } from '@/lib/location'
|
|
|
+
|
|
|
+import { getDictTypeApi } from '@/services/modules/common'
|
|
|
+import type { DictItem } from '@/services/modules/common/type'
|
|
|
+import { getPackageListByTaskTypeApi } from '@/services/modules/information'
|
|
|
+import type { SharePackageItem } from '@/services/modules/information/type'
|
|
|
+import { getListDrugTableApi } from '@/services/modules/task/taskFrom'
|
|
|
+import type { TaskFieldConfigItem } from '@/services/modules/task/taskFrom/type'
|
|
|
+import {
|
|
|
+ getSignListByUserIdApi,
|
|
|
+ getVisitTemplateApi,
|
|
|
+ isVisitTimeApi,
|
|
|
+ saveSignInfoApi,
|
|
|
+} from '@/services/modules/task/taskVisit'
|
|
|
+import type {
|
|
|
+ SaveSignInfoRequest,
|
|
|
+ VisitSignRecordItem,
|
|
|
+ VisitToType,
|
|
|
+} from '@/services/modules/task/taskVisit/type'
|
|
|
+
|
|
|
+import { useUserStore } from '@/stores/modules/user'
|
|
|
+
|
|
|
+import ImgUpload from '../../../task-form/components/ImgUpload.vue'
|
|
|
+import type { VisitLocationStatus, VisitTarget, VisitTargetType } from '../../types'
|
|
|
+
|
|
|
+type RequestStatus = 'idle' | 'loading' | 'ready' | 'error'
|
|
|
+
|
|
|
+interface StatusView {
|
|
|
+ badge: string
|
|
|
+ title: string
|
|
|
+ description: string
|
|
|
+}
|
|
|
+
|
|
|
+interface PackageSelection {
|
|
|
+ target: VisitTarget
|
|
|
+ packageItem: SharePackageItem
|
|
|
+}
|
|
|
+
|
|
|
+interface PackagePickerConfirmEvent {
|
|
|
+ selectedItems: SharePackageItem[]
|
|
|
+}
|
|
|
+
|
|
|
+interface TemplateFormState {
|
|
|
+ drug: string[]
|
|
|
+ purpose: string[]
|
|
|
+ result: Array<string | number>
|
|
|
+ visitor: string
|
|
|
+ department: string
|
|
|
+ photoUrls: string
|
|
|
+ rightHcp: string
|
|
|
+ rightInfo: string
|
|
|
+ rightActivity: string
|
|
|
+ remark: string
|
|
|
+}
|
|
|
+
|
|
|
+type TemplateEvaluationKey = 'rightHcp' | 'rightInfo' | 'rightActivity'
|
|
|
+
|
|
|
+const SAME_TARGET_COOLDOWN_SECONDS = 4 * 60 * 60
|
|
|
+const GLOBAL_COOLDOWN_SECONDS =
|
|
|
+ import.meta.env.VITE_MODE === 'production_luoxin' ? 60 * 60 : 30 * 60
|
|
|
+
|
|
|
+const TASK_TYPE_MAP: Record<VisitTargetType, string> = {
|
|
|
+ hospital: '5',
|
|
|
+ company: '6',
|
|
|
+ pharmacy: '33',
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ target?: VisitTarget
|
|
|
+ currentLocation: LocationResult | null
|
|
|
+ locationStatus: VisitLocationStatus
|
|
|
+ visitTo?: VisitToType
|
|
|
+}>()
|
|
|
+
|
|
|
+const emit = defineEmits<{
|
|
|
+ (event: 'open-visit-time'): void
|
|
|
+ (event: 'package-selected', payload: PackageSelection): void
|
|
|
+ (event: 'sign-success'): void
|
|
|
+}>()
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+const visitTimeStatus = ref<RequestStatus>('idle')
|
|
|
+const visitTimeAllowed = ref(false)
|
|
|
+const visitTimeMessage = ref('')
|
|
|
+const signRecordsStatus = ref<RequestStatus>('idle')
|
|
|
+const signRecords = ref<VisitSignRecordItem[]>([])
|
|
|
+const now = ref(Date.now())
|
|
|
+
|
|
|
+const packagePickerVisible = ref(false)
|
|
|
+const packageLoading = ref(false)
|
|
|
+const signSubmitting = ref(false)
|
|
|
+const packageList = ref<SharePackageItem[]>([])
|
|
|
+const selectedPackageValues = ref<Array<string | number>>([])
|
|
|
+
|
|
|
+const templateVisible = ref(false)
|
|
|
+const templateLoading = ref(false)
|
|
|
+const templateType = ref('')
|
|
|
+const templateTarget = ref<VisitTarget>()
|
|
|
+const templatePackageItem = ref<SharePackageItem>()
|
|
|
+const templateSignDate = ref('')
|
|
|
+const drugPickerVisible = ref(false)
|
|
|
+const purposePickerVisible = ref(false)
|
|
|
+const resultPickerVisible = ref(false)
|
|
|
+const drugOptions = ref<Array<{ label: string; value: string }>>([])
|
|
|
+const purposeOptions = ref<Array<{ label: string; value: string }>>([])
|
|
|
+const resultOptions = ref<Array<{ label: string; value: string }>>([])
|
|
|
+
|
|
|
+const createTemplateForm = (): TemplateFormState => ({
|
|
|
+ drug: [],
|
|
|
+ purpose: [],
|
|
|
+ result: [],
|
|
|
+ visitor: '',
|
|
|
+ department: '',
|
|
|
+ photoUrls: '',
|
|
|
+ rightHcp: '',
|
|
|
+ rightInfo: '',
|
|
|
+ rightActivity: '',
|
|
|
+ remark: '',
|
|
|
+})
|
|
|
+
|
|
|
+const templateForm = ref<TemplateFormState>(createTemplateForm())
|
|
|
+
|
|
|
+const templateEvaluationItems: Array<{ key: TemplateEvaluationKey; label: string }> = [
|
|
|
+ { key: 'rightHcp', label: '是否针对了正确的 HCP' },
|
|
|
+ { key: 'rightInfo', label: '是否传递了正确的信息' },
|
|
|
+ { key: 'rightActivity', label: '是否采取了正确的活动形式和频率' },
|
|
|
+]
|
|
|
+
|
|
|
+const templatePhotoFieldConfig = {
|
|
|
+ taskFiledValue: '现场照片',
|
|
|
+ isMustfill: '1',
|
|
|
+ taskFiledMinsize: 1,
|
|
|
+ taskFiledMaxsize: 5,
|
|
|
+} as TaskFieldConfigItem
|
|
|
+
|
|
|
+const deptId = computed(() => userStore.currentUserInfo?.deptId || '')
|
|
|
+const userId = computed(() => userStore.currentUserInfo?.userId || '')
|
|
|
+const targetName = computed(() => props.target?.name || '')
|
|
|
+const targetAddress = computed(() => props.target?.address || '暂无详细地址')
|
|
|
+
|
|
|
+const selectedPackage = computed(() => {
|
|
|
+ const selectedValue = selectedPackageValues.value[0]
|
|
|
+ return packageList.value.find((item) => String(item.value) === String(selectedValue))
|
|
|
+})
|
|
|
+
|
|
|
+const templateDrugText = computed(() => templateForm.value.drug.join('、') || '请选择')
|
|
|
+const templatePurposeText = computed(() => {
|
|
|
+ const selectedValues = new Set(templateForm.value.purpose.map(String))
|
|
|
+ return purposeOptions.value
|
|
|
+ .filter((item) => selectedValues.has(item.value))
|
|
|
+ .map((item) => item.label)
|
|
|
+ .join('、') || '请选择'
|
|
|
+})
|
|
|
+const templateResultText = computed(() => {
|
|
|
+ const selectedValue = templateForm.value.result[0]
|
|
|
+ return resultOptions.value.find((item) => item.value === String(selectedValue))?.label || '请选择'
|
|
|
+})
|
|
|
+const requiresDepartment = computed(() => {
|
|
|
+ return templateTarget.value?.type === 'hospital' && templateType.value !== 'TEMPLATE2'
|
|
|
+})
|
|
|
+const requiresTemplatePhoto = computed(() => {
|
|
|
+ const targetType = templateTarget.value?.type
|
|
|
+ return (
|
|
|
+ requiresDepartment.value ||
|
|
|
+ (targetType === 'company' && templateType.value === 'TEMPLATE2') ||
|
|
|
+ (targetType === 'pharmacy' && templateType.value === 'TEMPLATE3')
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+const cooldownDeadline = computed(() => {
|
|
|
+ const targetId = props.target?.signEntId
|
|
|
+ let latestSignTime = 0
|
|
|
+ let latestTargetSignTime = 0
|
|
|
+
|
|
|
+ for (const record of signRecords.value) {
|
|
|
+ const signTime = parseSignTime(record.signDate)
|
|
|
+ if (!signTime) continue
|
|
|
+
|
|
|
+ latestSignTime = Math.max(latestSignTime, signTime)
|
|
|
+ if (targetId && String(record.signEntId) === String(targetId)) {
|
|
|
+ latestTargetSignTime = Math.max(latestTargetSignTime, signTime)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return Math.max(
|
|
|
+ latestSignTime + GLOBAL_COOLDOWN_SECONDS * 1000,
|
|
|
+ latestTargetSignTime + SAME_TARGET_COOLDOWN_SECONDS * 1000
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+const remainingSeconds = computed(() => {
|
|
|
+ return Math.max(0, Math.ceil((cooldownDeadline.value - now.value) / 1000))
|
|
|
+})
|
|
|
+
|
|
|
+const countdownText = computed(() => formatCountdown(remainingSeconds.value))
|
|
|
+
|
|
|
+const canCheckIn = computed(() => {
|
|
|
+ return (
|
|
|
+ props.locationStatus === 'ready' &&
|
|
|
+ Boolean(props.currentLocation) &&
|
|
|
+ Boolean(props.target) &&
|
|
|
+ visitTimeStatus.value === 'ready' &&
|
|
|
+ visitTimeAllowed.value &&
|
|
|
+ signRecordsStatus.value === 'ready' &&
|
|
|
+ remainingSeconds.value === 0 &&
|
|
|
+ !packageLoading.value &&
|
|
|
+ !signSubmitting.value
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+const hasStatusError = computed(() => {
|
|
|
+ return (
|
|
|
+ props.locationStatus === 'error' ||
|
|
|
+ visitTimeStatus.value === 'error' ||
|
|
|
+ signRecordsStatus.value === 'error'
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+const isChecking = computed(() => {
|
|
|
+ return (
|
|
|
+ props.locationStatus === 'idle' ||
|
|
|
+ props.locationStatus === 'locating' ||
|
|
|
+ visitTimeStatus.value === 'idle' ||
|
|
|
+ visitTimeStatus.value === 'loading' ||
|
|
|
+ signRecordsStatus.value === 'idle' ||
|
|
|
+ signRecordsStatus.value === 'loading'
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+const statusView = computed<StatusView>(() => {
|
|
|
+ if (props.locationStatus !== 'ready') {
|
|
|
+ return {
|
|
|
+ badge: props.locationStatus === 'error' ? '不可打卡' : '校验中',
|
|
|
+ title: props.locationStatus === 'error' ? '定位失败' : '正在获取当前位置',
|
|
|
+ description: props.locationStatus === 'error' ? '请检查系统定位权限后重新定位。' : '',
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!props.target) {
|
|
|
+ return {
|
|
|
+ badge: '无拜访对象',
|
|
|
+ title: '暂无可用拜访对象',
|
|
|
+ description: '附近暂无该类型拜访对象',
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isChecking.value) {
|
|
|
+ return {
|
|
|
+ badge: '校验中',
|
|
|
+ title: '正在校验打卡条件',
|
|
|
+ description: '',
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hasStatusError.value || !visitTimeAllowed.value) {
|
|
|
+ return {
|
|
|
+ badge: '不可打卡',
|
|
|
+ title: '当前不可打卡',
|
|
|
+ description: hasStatusError.value
|
|
|
+ ? '打卡条件校验失败,请稍后重试。'
|
|
|
+ : visitTimeMessage.value || '当前不在允许的打卡时间内。',
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (remainingSeconds.value > 0) {
|
|
|
+ return {
|
|
|
+ badge: '冷却中',
|
|
|
+ title: '暂未到再次打卡时间',
|
|
|
+ description: `${countdownText.value} 后可再次打卡。`,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ badge: '可打卡',
|
|
|
+ title: '当前打卡地点',
|
|
|
+ description: '',
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const actionText = computed(() => {
|
|
|
+ if (remainingSeconds.value > 0) {
|
|
|
+ return { primary: countdownText.value, secondary: '后可打卡' }
|
|
|
+ }
|
|
|
+ if (packageLoading.value || signSubmitting.value) return { primary: '加载中', secondary: '' }
|
|
|
+ if (!canCheckIn.value) return { primary: '不可打卡', secondary: '' }
|
|
|
+ return { primary: '打卡', secondary: '' }
|
|
|
+})
|
|
|
+
|
|
|
+let clockTimer: ReturnType<typeof setInterval> | undefined
|
|
|
+
|
|
|
+const loadVisitTimeStatus = async () => {
|
|
|
+ const currentDeptId = deptId.value
|
|
|
+ const currentVisitTo = props.visitTo
|
|
|
+
|
|
|
+ if (!currentDeptId || !currentVisitTo) {
|
|
|
+ visitTimeStatus.value = 'idle'
|
|
|
+ visitTimeAllowed.value = false
|
|
|
+ visitTimeMessage.value = ''
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ visitTimeStatus.value = 'loading'
|
|
|
+ visitTimeAllowed.value = false
|
|
|
+ visitTimeMessage.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await isVisitTimeApi({
|
|
|
+ deptId: currentDeptId,
|
|
|
+ visitTo: currentVisitTo,
|
|
|
+ })
|
|
|
+
|
|
|
+ visitTimeAllowed.value = res.data === true
|
|
|
+ visitTimeStatus.value = 'ready'
|
|
|
+ visitTimeMessage.value = res.msg || ''
|
|
|
+ } catch {
|
|
|
+ visitTimeAllowed.value = false
|
|
|
+ visitTimeStatus.value = 'error'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadSignRecords = async () => {
|
|
|
+ const signUserid = userId.value
|
|
|
+
|
|
|
+ if (!signUserid) {
|
|
|
+ signRecords.value = []
|
|
|
+ signRecordsStatus.value = 'idle'
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ signRecordsStatus.value = 'loading'
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await getSignListByUserIdApi({ signUserid })
|
|
|
+
|
|
|
+ signRecords.value = Array.isArray(res.data) ? res.data : []
|
|
|
+ signRecordsStatus.value = 'ready'
|
|
|
+ now.value = Date.now()
|
|
|
+ } catch {
|
|
|
+ signRecords.value = []
|
|
|
+ signRecordsStatus.value = 'error'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleCheckInClick = async () => {
|
|
|
+ const target = props.target
|
|
|
+ if (!canCheckIn.value || !target) return
|
|
|
+ await openPackagePicker(target)
|
|
|
+}
|
|
|
+
|
|
|
+const openPackagePicker = async (target: VisitTarget) => {
|
|
|
+ packageLoading.value = true
|
|
|
+ packageList.value = []
|
|
|
+ selectedPackageValues.value = []
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await getPackageListByTaskTypeApi(TASK_TYPE_MAP[target.type])
|
|
|
+
|
|
|
+ if (props.target?.id !== target.id) return
|
|
|
+
|
|
|
+ const list = Array.isArray(res.data) ? res.data : []
|
|
|
+ if (list.length === 0) {
|
|
|
+ showToast('暂无可用积分包')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ packageList.value = list
|
|
|
+ selectedPackageValues.value = [list[0].value]
|
|
|
+ packagePickerVisible.value = true
|
|
|
+ } finally {
|
|
|
+ packageLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const confirmPackageSelection = async (event: PackagePickerConfirmEvent) => {
|
|
|
+ const target = props.target
|
|
|
+ const packageItem = event.selectedItems[0] || selectedPackage.value
|
|
|
+ if (!target || !packageItem) return
|
|
|
+
|
|
|
+ packagePickerVisible.value = false
|
|
|
+ emit('package-selected', { target, packageItem })
|
|
|
+
|
|
|
+ const taskTypeId = TASK_TYPE_MAP[target.type]
|
|
|
+ const hasTaskTypeTemplate = userStore.currentUserInfo?.hasTaskTypeTemplate || []
|
|
|
+
|
|
|
+ if (hasTaskTypeTemplate.includes(taskTypeId)) {
|
|
|
+ await openTemplateForm(target, packageItem, taskTypeId)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ await submitSign(target, packageItem)
|
|
|
+}
|
|
|
+
|
|
|
+const DICT_TYPE_MAP: Record<VisitTargetType, string> = {
|
|
|
+ hospital: 'user_sign_detail_purpose',
|
|
|
+ company: 'user_sign_detail_distribution_purpose',
|
|
|
+ pharmacy: 'user_sign_detail_pharmacy_purpose',
|
|
|
+}
|
|
|
+
|
|
|
+const toDictOptions = (list: DictItem[]) => {
|
|
|
+ return list.map((item) => ({ label: item.label, value: item.value }))
|
|
|
+}
|
|
|
+
|
|
|
+const openTemplateForm = async (
|
|
|
+ target: VisitTarget,
|
|
|
+ packageItem: SharePackageItem,
|
|
|
+ taskTypeId: string
|
|
|
+) => {
|
|
|
+ const currentDeptId = deptId.value
|
|
|
+ if (!currentDeptId) return
|
|
|
+
|
|
|
+ templateTarget.value = target
|
|
|
+ templatePackageItem.value = packageItem
|
|
|
+ templateSignDate.value = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ templateForm.value = createTemplateForm()
|
|
|
+ templateLoading.value = true
|
|
|
+ templateVisible.value = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ const templateRes = await getVisitTemplateApi({ deptId: currentDeptId, taskTypeId })
|
|
|
+ templateType.value = templateRes.data?.template || ''
|
|
|
+
|
|
|
+ const purposeDictType =
|
|
|
+ target.type === 'hospital' && templateType.value === 'TEMPLATE2'
|
|
|
+ ? 'user_sign_detail_template2_purpose'
|
|
|
+ : DICT_TYPE_MAP[target.type]
|
|
|
+
|
|
|
+ const [drugRes, purposeRes, resultRes] = await Promise.all([
|
|
|
+ getListDrugTableApi(String(packageItem.value)),
|
|
|
+ getDictTypeApi(purposeDictType),
|
|
|
+ getDictTypeApi('user_sign_detail_result'),
|
|
|
+ ])
|
|
|
+
|
|
|
+ if (templateTarget.value?.id !== target.id) return
|
|
|
+
|
|
|
+ drugOptions.value = (Array.isArray(drugRes.data) ? drugRes.data : []).map((item) => ({
|
|
|
+ label: String(item),
|
|
|
+ value: String(item),
|
|
|
+ }))
|
|
|
+ purposeOptions.value = toDictOptions(Array.isArray(purposeRes.data) ? purposeRes.data : [])
|
|
|
+ resultOptions.value = toDictOptions(Array.isArray(resultRes.data) ? resultRes.data : [])
|
|
|
+ } catch {
|
|
|
+ templateVisible.value = false
|
|
|
+ } finally {
|
|
|
+ templateLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openDrugPicker = () => {
|
|
|
+ if (drugOptions.value.length === 0) {
|
|
|
+ showToast('暂无可选推广药品')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ drugPickerVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const submitTemplateSign = async () => {
|
|
|
+ const target = templateTarget.value
|
|
|
+ const packageItem = templatePackageItem.value
|
|
|
+ if (!target || !packageItem) return
|
|
|
+
|
|
|
+ const form = templateForm.value
|
|
|
+ const result = String(form.result[0] ?? '')
|
|
|
+ const needsEvaluation = target.type === 'hospital' && templateType.value === 'TEMPLATE2'
|
|
|
+ const photoUrls = form.photoUrls
|
|
|
+ .split(',')
|
|
|
+ .map((item) => item.trim())
|
|
|
+ .filter(Boolean)
|
|
|
+
|
|
|
+ if (
|
|
|
+ form.drug.length === 0 ||
|
|
|
+ form.purpose.length === 0 ||
|
|
|
+ !result ||
|
|
|
+ (target.type === 'pharmacy' && !form.visitor.trim()) ||
|
|
|
+ (requiresDepartment.value && !form.department.trim()) ||
|
|
|
+ (requiresTemplatePhoto.value && photoUrls.length === 0) ||
|
|
|
+ (needsEvaluation && (!form.rightHcp || !form.rightInfo || !form.rightActivity))
|
|
|
+ ) {
|
|
|
+ showToast('请完善拜访信息')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const userSignDetail: Record<string, unknown> = {
|
|
|
+ templateType: templateType.value,
|
|
|
+ drug: form.drug,
|
|
|
+ purpose: form.purpose,
|
|
|
+ result,
|
|
|
+ remark: form.remark.trim(),
|
|
|
+ visitor: form.visitor.trim(),
|
|
|
+ detail: needsEvaluation
|
|
|
+ ? [
|
|
|
+ {
|
|
|
+ rightHcp: form.rightHcp === 'true',
|
|
|
+ rightInfo: form.rightInfo === 'true',
|
|
|
+ rightActivity: form.rightActivity === 'true',
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ : requiresDepartment.value
|
|
|
+ ? [{ departmentName: form.department.trim(), fileUrl: photoUrls }]
|
|
|
+ : [],
|
|
|
+ scenePhotoJson:
|
|
|
+ requiresTemplatePhoto.value && !requiresDepartment.value
|
|
|
+ ? [{ name: '', fileUrl: photoUrls }]
|
|
|
+ : [],
|
|
|
+ }
|
|
|
+
|
|
|
+ const submitted = await submitSign(target, packageItem, userSignDetail, templateSignDate.value)
|
|
|
+
|
|
|
+ if (submitted) {
|
|
|
+ templateVisible.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const resetTemplateForm = () => {
|
|
|
+ templateTarget.value = undefined
|
|
|
+ templatePackageItem.value = undefined
|
|
|
+ templateType.value = ''
|
|
|
+ templateSignDate.value = ''
|
|
|
+ templateForm.value = createTemplateForm()
|
|
|
+ drugOptions.value = []
|
|
|
+ purposeOptions.value = []
|
|
|
+ resultOptions.value = []
|
|
|
+}
|
|
|
+
|
|
|
+const submitSign = async (
|
|
|
+ target: VisitTarget,
|
|
|
+ packageItem: SharePackageItem,
|
|
|
+ userSignDetail: Record<string, unknown> = {},
|
|
|
+ signDate = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
|
+) => {
|
|
|
+ const location = props.currentLocation
|
|
|
+ const signUserid = userId.value
|
|
|
+ if (!location || !signUserid || signSubmitting.value) return false
|
|
|
+
|
|
|
+ const data: SaveSignInfoRequest = {
|
|
|
+ address: target.address,
|
|
|
+ latitude: location.latitude,
|
|
|
+ longitude: location.longitude,
|
|
|
+ packageId: String(packageItem.value),
|
|
|
+ signDate,
|
|
|
+ signEntId: target.signEntId,
|
|
|
+ signEntName: target.name,
|
|
|
+ signEntType: target.signEntType,
|
|
|
+ signUserid,
|
|
|
+ userSignDetail,
|
|
|
+ }
|
|
|
+
|
|
|
+ signSubmitting.value = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await saveSignInfoApi(data)
|
|
|
+
|
|
|
+ const message =
|
|
|
+ res.data?.type === '1' ? `提交成功,获得${res.data.score ?? 0}积分` : '完成任务数+1'
|
|
|
+
|
|
|
+ showToast(message, 3000)
|
|
|
+ packageList.value = []
|
|
|
+ selectedPackageValues.value = []
|
|
|
+ await loadSignRecords()
|
|
|
+ emit('sign-success')
|
|
|
+ return true
|
|
|
+ } catch {
|
|
|
+ return false
|
|
|
+ } finally {
|
|
|
+ signSubmitting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => [deptId.value, props.visitTo] as const,
|
|
|
+ () => void loadVisitTimeStatus(),
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+
|
|
|
+watch(userId, () => void loadSignRecords(), { immediate: true })
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.target?.id,
|
|
|
+ () => {
|
|
|
+ packagePickerVisible.value = false
|
|
|
+ templateVisible.value = false
|
|
|
+ packageLoading.value = false
|
|
|
+ packageList.value = []
|
|
|
+ selectedPackageValues.value = []
|
|
|
+ now.value = Date.now()
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ clockTimer = setInterval(() => {
|
|
|
+ now.value = Date.now()
|
|
|
+ }, 1000)
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (clockTimer) clearInterval(clockTimer)
|
|
|
+})
|
|
|
+
|
|
|
+const parseSignTime = (value: string) => {
|
|
|
+ if (!value) return 0
|
|
|
+
|
|
|
+ const timestamp = new Date(value.replace(/-/g, '/')).getTime()
|
|
|
+ return Number.isFinite(timestamp) ? timestamp : 0
|
|
|
+}
|
|
|
+
|
|
|
+const formatCountdown = (seconds: number) => {
|
|
|
+ const hours = Math.floor(seconds / 3600)
|
|
|
+ const minutes = Math.floor((seconds % 3600) / 60)
|
|
|
+ const restSeconds = seconds % 60
|
|
|
+
|
|
|
+ return [hours, minutes, restSeconds].map((value) => String(value).padStart(2, '0')).join(':')
|
|
|
+}
|
|
|
+
|
|
|
+const showToast = (title: string, duration?: number) => {
|
|
|
+ uni.showToast({
|
|
|
+ title,
|
|
|
+ icon: 'none',
|
|
|
+ ...(duration ? { duration } : {}),
|
|
|
+ })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.visit-status-card {
|
|
|
+ overflow: hidden;
|
|
|
+ background: linear-gradient(145deg, #ffffff 0%, #f7fbff 100%);
|
|
|
+ border: 2rpx solid rgba(47, 140, 247, 0.08);
|
|
|
+ border-radius: 30rpx;
|
|
|
+ box-shadow: 0 16rpx 48rpx rgba(31, 70, 112, 0.07);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__main {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 280rpx;
|
|
|
+ padding: 30rpx 26rpx 28rpx 30rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__copy {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ padding-right: 4rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__meta {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #718096;
|
|
|
+ font-size: 22rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 34rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__meta-dot {
|
|
|
+ width: 12rpx;
|
|
|
+ height: 12rpx;
|
|
|
+ margin-right: 10rpx;
|
|
|
+ background: #f4b94f;
|
|
|
+ border-radius: 50%;
|
|
|
+ box-shadow: 0 0 0 8rpx rgba(244, 185, 79, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__meta-dot--success {
|
|
|
+ background: #31b87b;
|
|
|
+ box-shadow: 0 0 0 8rpx rgba(49, 184, 123, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__meta-dot--error {
|
|
|
+ background: #e96868;
|
|
|
+ box-shadow: 0 0 0 8rpx rgba(233, 104, 104, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__badge {
|
|
|
+ height: 40rpx;
|
|
|
+ margin-left: 14rpx;
|
|
|
+ padding: 0 14rpx;
|
|
|
+ color: #b7791f;
|
|
|
+ font-size: 20rpx;
|
|
|
+ line-height: 40rpx;
|
|
|
+ background: #fff7e7;
|
|
|
+ border-radius: 999rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__badge--success {
|
|
|
+ color: #168456;
|
|
|
+ background: #e9f8f1;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__badge--error {
|
|
|
+ color: #c24f4f;
|
|
|
+ background: #fff0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__title {
|
|
|
+ margin-top: 18rpx;
|
|
|
+ color: #172033;
|
|
|
+ font-size: 34rpx;
|
|
|
+ font-weight: 750;
|
|
|
+ line-height: 46rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__desc {
|
|
|
+ max-width: 390rpx;
|
|
|
+ margin-top: 12rpx;
|
|
|
+ color: #7b8899;
|
|
|
+ font-size: 23rpx;
|
|
|
+ line-height: 36rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__place {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-top: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__place-copy {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ min-width: 0;
|
|
|
+ margin-left: 10rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__place-name {
|
|
|
+ overflow: hidden;
|
|
|
+ color: #2f4158;
|
|
|
+ font-size: 24rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 34rpx;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__place-address {
|
|
|
+ display: -webkit-box;
|
|
|
+ margin-top: 4rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ color: #8190a2;
|
|
|
+ font-size: 19rpx;
|
|
|
+ line-height: 28rpx;
|
|
|
+ word-break: break-all;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__action-wrap {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 18rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__action-wrap--disabled {
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__action-ring {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 176rpx;
|
|
|
+ height: 176rpx;
|
|
|
+ background: rgba(148, 163, 184, 0.12);
|
|
|
+ border: 2rpx solid rgba(148, 163, 184, 0.16);
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__action-ring--active {
|
|
|
+ background: rgba(47, 140, 247, 0.08);
|
|
|
+ border-color: rgba(47, 140, 247, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__action {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 146rpx;
|
|
|
+ height: 146rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 26rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 34rpx;
|
|
|
+ background: linear-gradient(145deg, #c9d0d9 0%, #aeb8c4 100%);
|
|
|
+ border-radius: 50%;
|
|
|
+ box-shadow: 0 14rpx 28rpx rgba(100, 116, 139, 0.16);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__action--active {
|
|
|
+ background: linear-gradient(145deg, #50b5ff 0%, #2f8cf7 100%);
|
|
|
+ box-shadow: 0 16rpx 32rpx rgba(47, 140, 247, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__action--countdown {
|
|
|
+ font-size: 21rpx;
|
|
|
+ line-height: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__footer {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 78rpx;
|
|
|
+ padding: 12rpx 28rpx;
|
|
|
+ background: rgba(239, 247, 255, 0.72);
|
|
|
+ border-top: 2rpx solid rgba(47, 140, 247, 0.06);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__footer-icon {
|
|
|
+ display: flex;
|
|
|
+ flex-shrink: 0;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 50rpx;
|
|
|
+ height: 50rpx;
|
|
|
+ margin-right: 16rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-status-card__footer-title {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ color: #2f8cf7;
|
|
|
+ font-size: 24rpx;
|
|
|
+ font-weight: 650;
|
|
|
+ line-height: 34rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ max-height: 88vh;
|
|
|
+ padding-bottom: env(safe-area-inset-bottom);
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 28rpx 30rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border-bottom: 2rpx solid #edf1f5;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__title {
|
|
|
+ color: #172033;
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ line-height: 44rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__subtitle {
|
|
|
+ max-width: 560rpx;
|
|
|
+ margin-top: 4rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ color: #8491a2;
|
|
|
+ font-size: 22rpx;
|
|
|
+ line-height: 32rpx;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__close {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ background: #f4f7fa;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__loading {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 420rpx;
|
|
|
+ color: #8491a2;
|
|
|
+ font-size: 26rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__scroll {
|
|
|
+ height: 860rpx;
|
|
|
+ padding-top: 18rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__field {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ min-height: 104rpx;
|
|
|
+ padding: 20rpx 30rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border-top: 2rpx solid #f1f3f6;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__field--vertical {
|
|
|
+ align-items: stretch;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__field-label {
|
|
|
+ margin-right: 24rpx;
|
|
|
+ color: #303a49;
|
|
|
+ font-size: 26rpx;
|
|
|
+ line-height: 38rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__field--vertical .visit-template__field-label {
|
|
|
+ margin: 0 0 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__required {
|
|
|
+ margin-right: 6rpx;
|
|
|
+ color: #e65d5d;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 18rpx;
|
|
|
+ padding: 20rpx 28rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ box-shadow: 0 -8rpx 24rpx rgba(31, 51, 73, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__button {
|
|
|
+ flex: 1;
|
|
|
+ height: 82rpx;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ line-height: 82rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__button::after {
|
|
|
+ border: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__button--cancel {
|
|
|
+ color: #53657a;
|
|
|
+ background: #eef2f6;
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__button--submit {
|
|
|
+ color: #ffffff;
|
|
|
+ background: linear-gradient(135deg, #50b5ff 0%, #2f8cf7 100%);
|
|
|
+}
|
|
|
+
|
|
|
+.visit-template__button--submit[disabled] {
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+</style>
|