Просмотр исходного кода

完成整体任务表单功能实现

yuanmingze 1 месяц назад
Родитель
Сommit
1de8ecc12b

+ 8 - 3
src/pages-task/task-form/components/DataTimeRange.vue

@@ -94,10 +94,15 @@ const calendarClick = () => {
 const handleConfirm = ({ value: selectedRange }: CalendarConfirmPayload) => {
 const handleConfirm = ({ value: selectedRange }: CalendarConfirmPayload) => {
   const [startTimestamp, endTimestamp] = selectedRange
   const [startTimestamp, endTimestamp] = selectedRange
 
 
-  if (!startTimestamp || !endTimestamp) return
+  if (!Number.isFinite(startTimestamp) || !Number.isFinite(endTimestamp)) return
 
 
-  const startDate = dayjs(startTimestamp).format(DATE_FORMAT)
-  const endDate = dayjs(endTimestamp).format(DATE_FORMAT)
+  const start = dayjs(startTimestamp)
+  const end = dayjs(endTimestamp)
+
+  if (!start.isValid() || !end.isValid()) return
+
+  const startDate = start.format(DATE_FORMAT)
+  const endDate = end.format(DATE_FORMAT)
   const formattedValue = `${startDate}${RANGE_SEPARATOR}${endDate}`
   const formattedValue = `${startDate}${RANGE_SEPARATOR}${endDate}`
 
 
   value.value = formattedValue
   value.value = formattedValue

+ 10 - 5
src/pages-task/task-form/components/ImgUpload.vue

@@ -126,7 +126,7 @@ const description = computed(() => {
 })
 })
 
 
 const modelText = computed(() => {
 const modelText = computed(() => {
-  return normalizeImageValue(value.value) ?? normalizeImageValue(label.value)
+  return normalizeImageValue(value.value) || normalizeImageValue(label.value)
 })
 })
 
 
 const imageList = computed<string[]>({
 const imageList = computed<string[]>({
@@ -175,6 +175,9 @@ const chooseImage = async () => {
     return
     return
   }
   }
 
 
+  uploading.value = true
+  let loadingVisible = false
+
   try {
   try {
     const source = await chooseUploadSource()
     const source = await chooseUploadSource()
     const count = source === 'camera' ? 1 : Math.min(remainingCount.value, 9)
     const count = source === 'camera' ? 1 : Math.min(remainingCount.value, 9)
@@ -182,12 +185,11 @@ const chooseImage = async () => {
 
 
     if (filePaths.length === 0) return
     if (filePaths.length === 0) return
 
 
-    uploading.value = true
-
     uni.showLoading({
     uni.showLoading({
       title: '上传中',
       title: '上传中',
       mask: true,
       mask: true,
     })
     })
+    loadingVisible = true
 
 
     const uploadedUrls: string[] = []
     const uploadedUrls: string[] = []
 
 
@@ -214,7 +216,10 @@ const chooseImage = async () => {
     })
     })
   } finally {
   } finally {
     uploading.value = false
     uploading.value = false
-    uni.hideLoading()
+
+    if (loadingVisible) {
+      uni.hideLoading()
+    }
   }
   }
 }
 }
 
 
@@ -534,7 +539,7 @@ const toSafeNumber = (value: unknown, fallback: number) => {
     return fallback
     return fallback
   }
   }
 
 
-  return numberValue
+  return Math.floor(numberValue)
 }
 }
 
 
 const isCancelError = (error: unknown) => {
 const isCancelError = (error: unknown) => {

+ 13 - 2
src/pages-task/task-form/components/InputAutoSelect.vue

@@ -13,7 +13,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { computed } from 'vue'
+import { computed, ref } from 'vue'
 
 
 import type { TaskFieldConfigItem } from '@/services/modules/task/taskFrom/type'
 import type { TaskFieldConfigItem } from '@/services/modules/task/taskFrom/type'
 
 
@@ -38,19 +38,30 @@ const cellValue = computed(() => {
 })
 })
 
 
 const isDisabled = computed(() => props.disabled)
 const isDisabled = computed(() => props.disabled)
+const opening = ref(false)
 
 
 const pickerClick = async () => {
 const pickerClick = async () => {
-  if (props.disabled) {
+  if (props.disabled || opening.value) {
     return
     return
   }
   }
 
 
+  opening.value = true
+
   try {
   try {
     const canOpen = await props.beforeOpen?.()
     const canOpen = await props.beforeOpen?.()
+
     if (canOpen === false) {
     if (canOpen === false) {
       return
       return
     }
     }
   } catch (error) {
   } catch (error) {
     console.error(`打开单选字段失败:${props.taskFieldConfig.taskFiledValue}`, error)
     console.error(`打开单选字段失败:${props.taskFieldConfig.taskFiledValue}`, error)
+
+    uni.showToast({
+      title: '选项加载失败,请稍后重试',
+      icon: 'none',
+    })
+  } finally {
+    opening.value = false
   }
   }
 }
 }
 </script>
 </script>

+ 5 - 1
src/pages-task/task-form/components/LongText.vue

@@ -49,7 +49,11 @@ const label = defineModel<FieldValue>('label', {
 })
 })
 
 
 const maxLength = computed(() => {
 const maxLength = computed(() => {
-  return Number(props.taskFieldConfig.taskFiledMaxsize) || 800
+  const configuredMaxLength = Number(props.taskFieldConfig.taskFiledMaxsize)
+
+  return Number.isFinite(configuredMaxLength) && configuredMaxLength > 0
+    ? Math.floor(configuredMaxLength)
+    : 800
 })
 })
 
 
 const textareaValue = computed({
 const textareaValue = computed({

+ 1 - 1
src/pages-task/task-form/components/MultipleSelect.vue

@@ -6,7 +6,7 @@
       :title="taskFieldConfig.taskFiledValue"
       :title="taskFieldConfig.taskFiledValue"
       :required="taskFieldConfig.isMustfill === '1'"
       :required="taskFieldConfig.isMustfill === '1'"
       :value="cellValue"
       :value="cellValue"
-      is-link
+      :is-link="!props.disabled"
       @click="pickerClick"
       @click="pickerClick"
     />
     />
 
 

+ 23 - 1
src/pages-task/task-form/components/QuestionnaireTask.vue

@@ -128,6 +128,11 @@ interface PickerConfirmEvent {
   selectedItems: PickerOption[]
   selectedItems: PickerOption[]
 }
 }
 
 
+interface QrCodeGenerationRequest {
+  questionnaireUrl: string
+  usesQuestionnaireList: boolean
+}
+
 const QR_CODE_CANVAS_ID = 'questionnaireQrCanvas'
 const QR_CODE_CANVAS_ID = 'questionnaireQrCanvas'
 const QR_CODE_SIZE = 280
 const QR_CODE_SIZE = 280
 const TASK_TYPE_ID = '801'
 const TASK_TYPE_ID = '801'
@@ -157,6 +162,7 @@ const qrCodeLoading = ref(false)
 let questionnaireRequestVersion = 0
 let questionnaireRequestVersion = 0
 let qrCodeGenerationVersion = 0
 let qrCodeGenerationVersion = 0
 let lastQrContent = ''
 let lastQrContent = ''
+let pendingQrCodeGeneration: QrCodeGenerationRequest | null = null
 
 
 const packageFieldLabel = computed(() => packageField.value?.taskFiledValue || '关联积分包')
 const packageFieldLabel = computed(() => packageField.value?.taskFiledValue || '关联积分包')
 
 
@@ -329,6 +335,7 @@ const generateManualQrCode = () => {
 
 
 const clearQrCode = () => {
 const clearQrCode = () => {
   qrCodeGenerationVersion += 1
   qrCodeGenerationVersion += 1
+  pendingQrCodeGeneration = null
   qrCodeImageUrl.value = ''
   qrCodeImageUrl.value = ''
   lastQrContent = ''
   lastQrContent = ''
 }
 }
@@ -336,7 +343,15 @@ const clearQrCode = () => {
 const generateQrCode = async (questionnaireUrl: string, usesQuestionnaireList: boolean) => {
 const generateQrCode = async (questionnaireUrl: string, usesQuestionnaireList: boolean) => {
   const packageId = selectedPackageId.value
   const packageId = selectedPackageId.value
 
 
-  if (!packageId || qrCodeLoading.value) {
+  if (!packageId) {
+    return
+  }
+
+  if (qrCodeLoading.value) {
+    pendingQrCodeGeneration = { questionnaireUrl, usesQuestionnaireList }
+    qrCodeGenerationVersion += 1
+    qrCodeImageUrl.value = ''
+    lastQrContent = ''
     return
     return
   }
   }
 
 
@@ -396,6 +411,13 @@ const generateQrCode = async (questionnaireUrl: string, usesQuestionnaireList: b
   } finally {
   } finally {
     qrCodeLoading.value = false
     qrCodeLoading.value = false
     hideGlobalLoading()
     hideGlobalLoading()
+
+    const pendingRequest = pendingQrCodeGeneration
+    pendingQrCodeGeneration = null
+
+    if (pendingRequest) {
+      void generateQrCode(pendingRequest.questionnaireUrl, pendingRequest.usesQuestionnaireList)
+    }
   }
   }
 }
 }
 
 

+ 55 - 13
src/pages-task/task-form/composables/useTaskForm.ts

@@ -31,6 +31,8 @@ const ON_SITE_VISIT_TASK_TYPE_ID = '85'
 const ON_SITE_VISIT_TYPE = '现场拜访'
 const ON_SITE_VISIT_TYPE = '现场拜访'
 const ON_SITE_VISIT_READONLY_FIELD_ALIASES = new Set(['level', 'visitType'])
 const ON_SITE_VISIT_READONLY_FIELD_ALIASES = new Set(['level', 'visitType'])
 
 
+export const MULTIPLE_SELECT_DRUG_TASK_TYPE_IDS = new Set(['1', '2', '3', '4', '64', '75'])
+
 /**
 /**
  * 单选、多选组件统一使用的选项结构。
  * 单选、多选组件统一使用的选项结构。
  *
  *
@@ -44,6 +46,7 @@ export interface TaskFieldOption {
 
 
 export type TaskFieldConfigViewItem = Omit<TaskFieldConfigItem, 'id'> & {
 export type TaskFieldConfigViewItem = Omit<TaskFieldConfigItem, 'id'> & {
   id: string
   id: string
+  originalTaskFiledType: string
   show: boolean
   show: boolean
   readonly: boolean
   readonly: boolean
   options: TaskFieldOption[] | null
   options: TaskFieldOption[] | null
@@ -107,7 +110,9 @@ const isOnSiteVisitField = (
   field: Pick<TaskFieldConfigItem, 'taskTypeId' | 'alias'>,
   field: Pick<TaskFieldConfigItem, 'taskTypeId' | 'alias'>,
   alias: string
   alias: string
 ): boolean => {
 ): boolean => {
-  return String(field.taskTypeId).trim() === ON_SITE_VISIT_TASK_TYPE_ID && field.alias?.trim() === alias
+  return (
+    String(field.taskTypeId).trim() === ON_SITE_VISIT_TASK_TYPE_ID && field.alias?.trim() === alias
+  )
 }
 }
 
 
 const shouldReadonlyField = (field: TaskFieldConfigItem): boolean => {
 const shouldReadonlyField = (field: TaskFieldConfigItem): boolean => {
@@ -117,10 +122,27 @@ const shouldReadonlyField = (field: TaskFieldConfigItem): boolean => {
   )
   )
 }
 }
 
 
+const shouldShowField = (field: TaskFieldConfigItem, deptId: string): boolean => {
+  // 旧页面仅在 longtext 分支读取 show,因此这里显式保留原有隐藏范围。
+  const hidesLongTextField =
+    String(field.taskTypeId).trim() === '18' &&
+    field.taskFiledType.trim() === 'longtext' &&
+    HIDE_FIELD_DEPT_ID.includes(deptId)
+
+  return !hidesLongTextField
+}
+
+export const isMultipleSelectDrugField = (
+  field: Pick<TaskFieldConfigItem, 'taskTypeId' | 'alias'>
+): boolean => {
+  return (
+    MULTIPLE_SELECT_DRUG_TASK_TYPE_IDS.has(String(field.taskTypeId).trim()) &&
+    field.alias?.trim() === 'drugs'
+  )
+}
+
 const serializeTaskFieldType = (field: TaskFieldConfigItem): string => {
 const serializeTaskFieldType = (field: TaskFieldConfigItem): string => {
-  if (String(field.taskTypeId).trim() === '75' && field.alias?.trim() === 'drugs') {
-    return 'multiple_select'
-  }
+  if (isMultipleSelectDrugField(field)) return 'multiple_select'
 
 
   return field.taskFiledType
   return field.taskFiledType
 }
 }
@@ -234,13 +256,15 @@ const getFieldDictOptions = (
  */
  */
 const serializeTaskFieldConfig = (
 const serializeTaskFieldConfig = (
   config: TaskFieldConfigItem[],
   config: TaskFieldConfigItem[],
-  dict: TaskFieldDictMap
+  dict: TaskFieldDictMap,
+  deptId: string
 ): TaskFieldConfigViewItem[] => {
 ): TaskFieldConfigViewItem[] => {
   return config.map((field) => ({
   return config.map((field) => ({
     ...field,
     ...field,
     id: String(field.id),
     id: String(field.id),
+    originalTaskFiledType: field.taskFiledType,
     taskFiledType: serializeTaskFieldType(field),
     taskFiledType: serializeTaskFieldType(field),
-    show: true,
+    show: shouldShowField(field, deptId),
     readonly: shouldReadonlyField(field),
     readonly: shouldReadonlyField(field),
     options: getFieldDictOptions(field, dict),
     options: getFieldDictOptions(field, dict),
   }))
   }))
@@ -341,6 +365,7 @@ export const useTaskForm = () => {
   const taskFormConfigMap = new Map<string, TaskFieldConfigViewItem[]>()
   const taskFormConfigMap = new Map<string, TaskFieldConfigViewItem[]>()
 
 
   let deriveFrom: TaskContentConfigDeriveFrom | null = null
   let deriveFrom: TaskContentConfigDeriveFrom | null = null
+  let loadRequestVersion = 0
 
 
   const createFormState = (config: TaskFieldConfigViewItem[]): DynamicFormState => {
   const createFormState = (config: TaskFieldConfigViewItem[]): DynamicFormState => {
     const formValue: DynamicFormState['value'] = {}
     const formValue: DynamicFormState['value'] = {}
@@ -352,9 +377,6 @@ export const useTaskForm = () => {
     const initialDateTime = dayjs()
     const initialDateTime = dayjs()
 
 
     for (const field of config) {
     for (const field of config) {
-      if (field.taskTypeId === '18' && HIDE_FIELD_DEPT_ID.includes(deptId)) {
-        field.show = false
-      }
       formValue[field.id] = createInitialFieldValue(field, initialDateTime)
       formValue[field.id] = createInitialFieldValue(field, initialDateTime)
     }
     }
 
 
@@ -363,7 +385,7 @@ export const useTaskForm = () => {
     }
     }
   }
   }
 
 
-  const resetTaskForm = (): void => {
+  const clearTaskFormState = (): void => {
     form.value = createEmptyFormState()
     form.value = createEmptyFormState()
     taskFieldConfigList.value = []
     taskFieldConfigList.value = []
     activeTaskTypeId.value = ''
     activeTaskTypeId.value = ''
@@ -372,6 +394,11 @@ export const useTaskForm = () => {
     deriveFrom = null
     deriveFrom = null
   }
   }
 
 
+  const resetTaskForm = (): void => {
+    loadRequestVersion += 1
+    clearTaskFormState()
+  }
+
   const activateTaskForm = (
   const activateTaskForm = (
     taskTypeId: string,
     taskTypeId: string,
     derivedFieldValue?: DynamicFormFieldValue
     derivedFieldValue?: DynamicFormFieldValue
@@ -419,7 +446,11 @@ export const useTaskForm = () => {
       return
       return
     }
     }
 
 
-    const config = serializeTaskFieldConfig(taskContentConfig.config, taskContentConfig.dict)
+    const config = serializeTaskFieldConfig(
+      taskContentConfig.config,
+      taskContentConfig.dict,
+      deptId
+    )
 
 
     taskFormConfigMap.set(normalizedTaskTypeId, config)
     taskFormConfigMap.set(normalizedTaskTypeId, config)
   }
   }
@@ -519,12 +550,19 @@ export const useTaskForm = () => {
       throw new Error('缺少 taskTypeId')
       throw new Error('缺少 taskTypeId')
     }
     }
 
 
+    const requestVersion = ++loadRequestVersion
+
     try {
     try {
       const res = await getTaskContentConfigByTaskTypeIdApi(normalizedTaskTypeId)
       const res = await getTaskContentConfigByTaskTypeIdApi(normalizedTaskTypeId)
+
+      if (requestVersion !== loadRequestVersion) {
+        return
+      }
+
       const responseData = res.data
       const responseData = res.data
       const singleTaskContentConfig = serializeTaskContentConfigResponse(responseData)
       const singleTaskContentConfig = serializeTaskContentConfigResponse(responseData)
 
 
-      resetTaskForm()
+      clearTaskFormState()
 
 
       if (singleTaskContentConfig) {
       if (singleTaskContentConfig) {
         loadSingleTaskForm(normalizedTaskTypeId, singleTaskContentConfig)
         loadSingleTaskForm(normalizedTaskTypeId, singleTaskContentConfig)
@@ -535,7 +573,11 @@ export const useTaskForm = () => {
         throw new Error('动态表单配置格式无效')
         throw new Error('动态表单配置格式无效')
       }
       }
     } catch (error) {
     } catch (error) {
-      resetTaskForm()
+      if (requestVersion !== loadRequestVersion) {
+        return
+      }
+
+      clearTaskFormState()
       throw error
       throw error
     }
     }
   }
   }

+ 52 - 15
src/pages-task/task-form/composables/useTaskFormAction.ts

@@ -1,4 +1,4 @@
-import { type Ref, ref } from 'vue'
+import { onScopeDispose, type Ref, ref } from 'vue'
 
 
 import { draftTaskContentApi, saveTaskContentApi } from '@/services/modules/task/taskFrom'
 import { draftTaskContentApi, saveTaskContentApi } from '@/services/modules/task/taskFrom'
 import type {
 import type {
@@ -7,7 +7,11 @@ import type {
   SaveTaskContentSuccessData,
   SaveTaskContentSuccessData,
 } from '@/services/modules/task/taskFrom/type'
 } from '@/services/modules/task/taskFrom/type'
 
 
-import type { DynamicFormState, TaskFieldConfigViewItem } from './useTaskForm'
+import {
+  type DynamicFormState,
+  isMultipleSelectDrugField,
+  type TaskFieldConfigViewItem,
+} from './useTaskForm'
 
 
 interface UseTaskFormActionOptions {
 interface UseTaskFormActionOptions {
   taskTypeId: Ref<string>
   taskTypeId: Ref<string>
@@ -17,7 +21,7 @@ interface UseTaskFormActionOptions {
 
 
 type FormAction = 'save' | 'submit'
 type FormAction = 'save' | 'submit'
 
 
-const PACKAGE_FIELD_ALIASES = new Set(['approvalResult', 'scorePackage'])
+const PACKAGE_FIELD_ALIASES = ['scorePackage', 'approvalResult'] as const
 const IMAGE_FIELD_TYPE = 'img'
 const IMAGE_FIELD_TYPE = 'img'
 const SUCCESS_TOAST_DURATION = 3000
 const SUCCESS_TOAST_DURATION = 3000
 
 
@@ -109,13 +113,26 @@ export const useTaskFormAction = ({
   form,
   form,
   taskFieldConfigList,
   taskFieldConfigList,
 }: UseTaskFormActionOptions) => {
 }: UseTaskFormActionOptions) => {
-  let actionLocked = false
-
+  const isActionPending = ref(false)
   const duplicateImageVisible = ref(false)
   const duplicateImageVisible = ref(false)
   const duplicateImageList = ref<string[]>([])
   const duplicateImageList = ref<string[]>([])
+  let navigateBackTimer: ReturnType<typeof setTimeout> | null = null
+
+  onScopeDispose(() => {
+    if (navigateBackTimer) {
+      clearTimeout(navigateBackTimer)
+      navigateBackTimer = null
+    }
+  })
 
 
   const findPackageField = (): TaskFieldConfigViewItem | undefined => {
   const findPackageField = (): TaskFieldConfigViewItem | undefined => {
-    return taskFieldConfigList.value.find((field) => PACKAGE_FIELD_ALIASES.has(field.alias ?? ''))
+    for (const alias of PACKAGE_FIELD_ALIASES) {
+      const field = taskFieldConfigList.value.find((item) => item.alias === alias)
+
+      if (field) return field
+    }
+
+    return undefined
   }
   }
 
 
   const getPackageId = (): string => {
   const getPackageId = (): string => {
@@ -199,6 +216,8 @@ export const useTaskFormAction = ({
 
 
   const validateSubmitForm = (): boolean => {
   const validateSubmitForm = (): boolean => {
     for (const field of taskFieldConfigList.value) {
     for (const field of taskFieldConfigList.value) {
+      if (!field.show) continue
+
       if (!validateField(field)) {
       if (!validateField(field)) {
         return false
         return false
       }
       }
@@ -218,6 +237,11 @@ export const useTaskFormAction = ({
     const packageId = getPackageId()
     const packageId = getPackageId()
 
 
     if (action === 'submit' && !packageId) {
     if (action === 'submit' && !packageId) {
+      if (taskFieldConfigList.value.some(isMultipleSelectDrugField)) {
+        showMessage('请先选择积分包')
+        return null
+      }
+
       const packageField = findPackageField()
       const packageField = findPackageField()
 
 
       showMessage(
       showMessage(
@@ -237,6 +261,8 @@ export const useTaskFormAction = ({
     const payload: SaveTaskContentBody = {}
     const payload: SaveTaskContentBody = {}
 
 
     for (const field of taskFieldConfigList.value) {
     for (const field of taskFieldConfigList.value) {
+      if (!field.show) continue
+
       const fieldValue = form.value.value[field.id]
       const fieldValue = form.value.value[field.id]
 
 
       payload[field.taskFiledValue] = {
       payload[field.taskFiledValue] = {
@@ -251,7 +277,7 @@ export const useTaskFormAction = ({
         items: [
         items: [
           {
           {
             label: String(fieldValue?.label ?? ''),
             label: String(fieldValue?.label ?? ''),
-            type: field.taskFiledType,
+            type: field.originalTaskFiledType,
             value: String(fieldValue?.value ?? ''),
             value: String(fieldValue?.value ?? ''),
           },
           },
         ],
         ],
@@ -270,29 +296,37 @@ export const useTaskFormAction = ({
     duplicateImageVisible.value = true
     duplicateImageVisible.value = true
   }
   }
 
 
-  const handleActionSuccess = (action: FormAction, data: unknown): void => {
+  const handleActionSuccess = (action: FormAction, data: unknown): boolean => {
     if (action === 'save') {
     if (action === 'save') {
       showMessage('保存成功', SUCCESS_TOAST_DURATION)
       showMessage('保存成功', SUCCESS_TOAST_DURATION)
     } else {
     } else {
       if (!isSaveTaskContentSuccessData(data)) {
       if (!isSaveTaskContentSuccessData(data)) {
         showMessage('提交结果数据异常')
         showMessage('提交结果数据异常')
-        return
+        return false
       }
       }
 
 
       showMessage(`提交成功,若审核通过可获得${data.score}积分`, SUCCESS_TOAST_DURATION)
       showMessage(`提交成功,若审核通过可获得${data.score}积分`, SUCCESS_TOAST_DURATION)
     }
     }
 
 
-    setTimeout(() => {
-      uni.navigateBack()
+    navigateBackTimer = setTimeout(() => {
+      navigateBackTimer = null
+      uni.navigateBack({
+        complete: () => {
+          isActionPending.value = false
+        },
+      })
     }, SUCCESS_TOAST_DURATION)
     }, SUCCESS_TOAST_DURATION)
+
+    return true
   }
   }
 
 
   const executeAction = async (action: FormAction): Promise<void> => {
   const executeAction = async (action: FormAction): Promise<void> => {
-    if (actionLocked) {
+    if (isActionPending.value) {
       return
       return
     }
     }
 
 
-    actionLocked = true
+    isActionPending.value = true
+    let navigationScheduled = false
 
 
     try {
     try {
       if (action === 'submit' && !validateSubmitForm()) {
       if (action === 'submit' && !validateSubmitForm()) {
@@ -333,13 +367,15 @@ export const useTaskFormAction = ({
         return
         return
       }
       }
 
 
-      handleActionSuccess(action, response.data)
+      navigationScheduled = handleActionSuccess(action, response.data)
     } catch (error: unknown) {
     } catch (error: unknown) {
       console.error(`[useTaskFormAction] ${action} 失败`, error)
       console.error(`[useTaskFormAction] ${action} 失败`, error)
 
 
       showMessage(ACTION_FAILURE_TEXT[action])
       showMessage(ACTION_FAILURE_TEXT[action])
     } finally {
     } finally {
-      actionLocked = false
+      if (!navigationScheduled) {
+        isActionPending.value = false
+      }
     }
     }
   }
   }
 
 
@@ -352,6 +388,7 @@ export const useTaskFormAction = ({
   }
   }
 
 
   return {
   return {
+    isActionPending,
     duplicateImageVisible,
     duplicateImageVisible,
     duplicateImageList,
     duplicateImageList,
     clearDuplicateImages,
     clearDuplicateImages,

+ 130 - 0
src/pages-task/task-form/composables/useTaskFormFields.ts

@@ -0,0 +1,130 @@
+import type { Ref } from 'vue'
+
+import type {
+  DynamicFormFieldValue,
+  DynamicFormState,
+  TaskFieldConfigViewItem,
+  TaskFieldOption,
+} from './useTaskForm'
+
+interface UseTaskFormFieldsOptions {
+  form: Ref<DynamicFormState>
+  taskFieldConfigList: Ref<TaskFieldConfigViewItem[]>
+}
+
+export type TaskFieldValuePatch = Partial<DynamicFormFieldValue>
+
+const normalizeFieldText = (value: unknown): string => {
+  return value === undefined || value === null ? '' : String(value)
+}
+
+/**
+ * 集中管理动态表单字段的查找和更新,避免联动逻辑只修改展示文本,
+ * 却遗留旧提交值。
+ */
+export const useTaskFormFields = ({ form, taskFieldConfigList }: UseTaskFormFieldsOptions) => {
+  const findFieldByAlias = (alias: string): TaskFieldConfigViewItem | undefined => {
+    return taskFieldConfigList.value.find((field) => field.alias === alias)
+  }
+
+  const getFieldFormValue = (field: TaskFieldConfigViewItem): DynamicFormFieldValue | undefined => {
+    return form.value.value[field.id]
+  }
+
+  const getFieldFormValueByAlias = (alias: string): DynamicFormFieldValue | undefined => {
+    const field = findFieldByAlias(alias)
+
+    return field ? getFieldFormValue(field) : undefined
+  }
+
+  const getFieldValueByAlias = (alias: string): string | undefined => {
+    return getFieldFormValueByAlias(alias)?.value
+  }
+
+  const setFieldFormValue = (
+    field: TaskFieldConfigViewItem,
+    fieldValue: TaskFieldValuePatch
+  ): void => {
+    const currentValue = getFieldFormValue(field)
+
+    if (!currentValue) return
+
+    if (fieldValue.label !== undefined) {
+      currentValue.label = fieldValue.label
+    }
+
+    if (fieldValue.value !== undefined) {
+      currentValue.value = fieldValue.value
+    }
+  }
+
+  const setFieldFormValueByAlias = (alias: string, fieldValue: TaskFieldValuePatch): void => {
+    const field = findFieldByAlias(alias)
+
+    if (field) {
+      setFieldFormValue(field, fieldValue)
+    }
+  }
+
+  /**
+   * 同步写入展示文本和提交值;若文本命中字典选项,则提交对应的 option value。
+   */
+  const setFieldTextByAlias = (alias: string, value: unknown): void => {
+    const field = findFieldByAlias(alias)
+
+    if (!field) return
+
+    const normalizedValue = normalizeFieldText(value)
+    const matchedOption = field.options?.find((option) => option.label === normalizedValue)
+
+    setFieldFormValue(field, {
+      label: matchedOption?.label ?? normalizedValue,
+      value: matchedOption?.value ?? normalizedValue,
+    })
+  }
+
+  const clearFieldValue = (field: TaskFieldConfigViewItem): void => {
+    setFieldFormValue(field, { label: '', value: '' })
+  }
+
+  const clearFieldValueByAlias = (alias: string): void => {
+    const field = findFieldByAlias(alias)
+
+    if (field) {
+      clearFieldValue(field)
+    }
+  }
+
+  const clearFieldValuesByAlias = (aliases: Iterable<string>): void => {
+    for (const alias of aliases) {
+      clearFieldValueByAlias(alias)
+    }
+  }
+
+  const setFieldOptions = (field: TaskFieldConfigViewItem, options: TaskFieldOption[]): void => {
+    field.options = options.length ? options : null
+  }
+
+  const setFieldOptionsByAlias = (alias: string, options: TaskFieldOption[]): void => {
+    const field = findFieldByAlias(alias)
+
+    if (field) {
+      setFieldOptions(field, options)
+    }
+  }
+
+  return {
+    findFieldByAlias,
+    getFieldFormValue,
+    getFieldFormValueByAlias,
+    getFieldValueByAlias,
+    setFieldFormValue,
+    setFieldFormValueByAlias,
+    setFieldTextByAlias,
+    clearFieldValue,
+    clearFieldValueByAlias,
+    clearFieldValuesByAlias,
+    setFieldOptions,
+    setFieldOptionsByAlias,
+  }
+}

+ 267 - 412
src/pages-task/task-form/composables/useTaskSelectHandlers.ts

@@ -14,13 +14,13 @@ import {
 import type { PackageRuleTaskTypeItem, TaskTypeItem } from '@/services/modules/task/taskFrom/type'
 import type { PackageRuleTaskTypeItem, TaskTypeItem } from '@/services/modules/task/taskFrom/type'
 
 
 import type {
 import type {
-  DynamicFormFieldValue,
   DynamicFormState,
   DynamicFormState,
   FieldValue,
   FieldValue,
   TaskFieldConfigViewItem,
   TaskFieldConfigViewItem,
   TaskFieldOption,
   TaskFieldOption,
 } from './useTaskForm'
 } from './useTaskForm'
-import { serializeTaskFieldOptions } from './useTaskForm'
+import { isMultipleSelectDrugField, serializeTaskFieldOptions } from './useTaskForm'
+import { useTaskFormFields } from './useTaskFormFields'
 
 
 export interface SingleSelectChangeEvent {
 export interface SingleSelectChangeEvent {
   previousValue: FieldValue
   previousValue: FieldValue
@@ -36,6 +36,68 @@ interface UseTaskSelectHandlersOptions {
   taskTypes: Ref<TaskTypeItem[]>
   taskTypes: Ref<TaskTypeItem[]>
 }
 }
 
 
+const MEETING_TASK_TYPE_IDS = new Set(['1', '2', '3', '4', '64'])
+const SURVEY_TARGET_TASK_TYPE_IDS = new Set(['14', '31', '37'])
+const HOSPITAL_OR_PHARMACY_TASK_TYPE_IDS = new Set(['75', '76'])
+const ENTITY_SELECTOR_URL = '/pages-common/entity-selector/index'
+const PHARMACY_AUTOFILL_FIELD_ALIASES = ['pharmacyType', 'address', 'shengshiqu']
+const HOSPITAL_AUTOFILL_FIELD_ALIASES = [
+  'city',
+  'address',
+  'level',
+  'ownerType',
+  'website',
+  'bedNumber',
+]
+const COMPANY_AUTOFILL_FIELD_ALIASES = [
+  'address',
+  'gsgm',
+  'shyshxydm',
+  'zczb',
+  'frdb',
+  'medicineTraceability',
+  'medicineTraceabilityOther',
+  'gsjj',
+  'jyys',
+  'registerAddress',
+]
+
+const PACKAGE_DEPENDENT_FIELD_ALIASES: Readonly<Record<string, readonly string[]>> = {
+  '1': ['drugs'],
+  '2': ['drugs'],
+  '3': ['drugs'],
+  '4': ['drugs'],
+  '47': ['drugs', 'realTaskTypeId'],
+  '64': ['drugs'],
+  '75': ['drugs', 'hospitalOrPharmacy', 'hospitalOrPharmacyAddress'],
+  '76': ['hospitalOrPharmacy', 'hospitalOrPharmacyAddress'],
+  '81': ['hospitalOrPharmacy', 'address'],
+  '85': ['hospital', 'level'],
+}
+
+const normalizeTaskTypeId = (taskTypeId: unknown): string => {
+  return String(taskTypeId ?? '').trim()
+}
+
+const hasTaskType = (field: TaskFieldConfigViewItem, taskTypeIds: ReadonlySet<string>): boolean => {
+  return taskTypeIds.has(normalizeTaskTypeId(field.taskTypeId))
+}
+
+const showToast = (title: string): void => {
+  uni.showToast({ title, icon: 'none' })
+}
+
+const formatRegion = (province: string, city: string, area: string): string => {
+  return [province, city, area].filter(Boolean).join('-')
+}
+
+const formatDetailedAddress = (item: SurveyTargetItem): string => {
+  const region = [item.province, item.city, item.area].filter(Boolean).join('')
+  const address = item.address?.trim() ?? ''
+
+  return [region, address].filter(Boolean).join('-')
+}
+
 /**
 /**
  * 动态表单选择器业务处理。
  * 动态表单选择器业务处理。
  *
  *
@@ -53,122 +115,92 @@ export const useTaskSelectHandlers = ({
   taskFieldConfigList,
   taskFieldConfigList,
   taskTypes,
   taskTypes,
 }: UseTaskSelectHandlersOptions) => {
 }: UseTaskSelectHandlersOptions) => {
-  /**
-   * 根据 alias 查找字段配置。
-   */
-  const findFieldByAlias = (alias: string): TaskFieldConfigViewItem | undefined => {
-    return taskFieldConfigList.value.find((item) => item.alias === alias)
-  }
-
-  /**
-   * 根据字段配置获取对应表单值。
-   */
-  const getFieldFormValue = (field: TaskFieldConfigViewItem): DynamicFormFieldValue | undefined => {
-    return form.value.value[field.id]
-  }
-
-  /**
-   * 根据 alias 获取对应表单值。
-   */
-  const getFieldFormValueByAlias = (alias: string): DynamicFormFieldValue | undefined => {
-    const field = findFieldByAlias(alias)
-
-    if (!field) {
-      return undefined
-    }
-
-    return getFieldFormValue(field)
-  }
+  const {
+    findFieldByAlias,
+    getFieldFormValue,
+    getFieldFormValueByAlias,
+    getFieldValueByAlias,
+    setFieldFormValue,
+    setFieldFormValueByAlias,
+    setFieldTextByAlias,
+    clearFieldValue,
+    clearFieldValueByAlias,
+    clearFieldValuesByAlias,
+    setFieldOptions,
+    setFieldOptionsByAlias,
+  } = useTaskFormFields({ form, taskFieldConfigList })
+
+  const fieldOptionsRequestVersions = new Map<string, number>()
+
+  const openEntitySelector = <T>(
+    alias: string,
+    eventName: string,
+    onSelect: (selectedItem: T) => void
+  ): false => {
+    uni.navigateTo({
+      url: `${ENTITY_SELECTOR_URL}?alias=${encodeURIComponent(alias)}`,
+      events: {
+        [eventName]: onSelect,
+      },
+    })
 
 
-  /**
-   * 根据 alias 获取字段实际 value。
-   */
-  const getFieldValueByAlias = (alias: string): FieldValue | undefined => {
-    return getFieldFormValueByAlias(alias)?.value
+    return false
   }
   }
 
 
-  /**
-   * 更新指定字段的表单值。
-   */
-  const setFieldFormValue = (
-    field: TaskFieldConfigViewItem,
-    fieldValue: Partial<DynamicFormFieldValue>
-  ) => {
-    const currentValue = getFieldFormValue(field)
-
-    if (!currentValue) {
-      return
-    }
+  const requireFieldValue = (alias: string, message: string): string | null => {
+    const value = getFieldValueByAlias(alias)?.trim()
 
 
-    if (fieldValue.label !== undefined) {
-      currentValue.label = fieldValue.label
-    }
+    if (value) return value
 
 
-    if (fieldValue.value !== undefined) {
-      currentValue.value = fieldValue.value
-    }
+    showToast(message)
+    return null
   }
   }
 
 
-  /**
-   * 根据 alias 更新字段表单值。
-   */
-  const setFieldFormValueByAlias = (alias: string, fieldValue: Partial<DynamicFormFieldValue>) => {
+  const invalidateFieldOptionsByAlias = (alias: string): void => {
     const field = findFieldByAlias(alias)
     const field = findFieldByAlias(alias)
 
 
-    if (!field) {
-      return
-    }
+    if (!field) return
 
 
-    setFieldFormValue(field, fieldValue)
+    fieldOptionsRequestVersions.set(field.id, (fieldOptionsRequestVersions.get(field.id) ?? 0) + 1)
+    setFieldOptions(field, [])
   }
   }
 
 
-  /**
-   * 清空字段值。
-   */
-  const clearFieldValue = (field: TaskFieldConfigViewItem) => {
-    const currentValue = getFieldFormValue(field)
-
-    if (!currentValue) {
-      return
-    }
+  const loadLatestFieldOptions = async (
+    field: TaskFieldConfigViewItem,
+    loader: () => Promise<TaskFieldOption[]>
+  ): Promise<boolean> => {
+    const requestVersion = (fieldOptionsRequestVersions.get(field.id) ?? 0) + 1
 
 
-    currentValue.label = ''
-    currentValue.value = ''
-  }
+    fieldOptionsRequestVersions.set(field.id, requestVersion)
 
 
-  /**
-   * 根据 alias 清空字段值。
-   */
-  const clearFieldValueByAlias = (alias: string) => {
-    const field = findFieldByAlias(alias)
+    const options = await loader()
+    const isCurrentField = taskFieldConfigList.value.includes(field)
 
 
-    if (!field) {
-      return
+    if (fieldOptionsRequestVersions.get(field.id) !== requestVersion || !isCurrentField) {
+      return false
     }
     }
 
 
-    clearFieldValue(field)
-  }
-
-  /**
-   * 更新字段选项。
-   *
-   * 没有选项时统一设置为 null。
-   */
-  const setFieldOptions = (field: TaskFieldConfigViewItem, options: TaskFieldOption[]) => {
-    field.options = options.length ? options : null
+    setFieldOptions(field, options)
+    return true
   }
   }
 
 
-  /**
-   * 根据 alias 更新字段选项。
-   */
-  const setFieldOptionsByAlias = (alias: string, options: TaskFieldOption[]) => {
-    const field = findFieldByAlias(alias)
+  const openSurveyTargetSelector = (
+    targetAlias: 'surveyTarget' | 'hospitalOrPharmacy',
+    addressAlias?: string
+  ): false => {
+    return openEntitySelector<SurveyTargetItem>(
+      'surveyTarget',
+      'surveyTargetSelect',
+      (selectedItem) => {
+        const name = selectedItem.yymc || selectedItem.gsmc || selectedItem.pharmacyName || ''
 
 
-    if (!field) {
-      return
-    }
+        setFieldTextByAlias(targetAlias, name)
 
 
-    setFieldOptions(field, options)
+        if (addressAlias) {
+          setFieldTextByAlias(addressAlias, formatDetailedAddress(selectedItem))
+        }
+      }
+    )
   }
   }
 
 
   const otcScoreList = ref<PackageRuleTaskTypeItem[]>([])
   const otcScoreList = ref<PackageRuleTaskTypeItem[]>([])
@@ -235,165 +267,85 @@ export const useTaskSelectHandlers = ({
    * 特殊接口请求和业务校验写在这里。
    * 特殊接口请求和业务校验写在这里。
    */
    */
   const handleSingleSelectBeforeOpen = async (field: TaskFieldConfigViewItem): Promise<boolean> => {
   const handleSingleSelectBeforeOpen = async (field: TaskFieldConfigViewItem): Promise<boolean> => {
-    if (['1', '2', '3', '4', '64'].includes(field.taskTypeId) && field.alias === 'scorePackage') {
-      const meetingTypeValue = getFieldFormValueByAlias('meetingType')
-      if (!meetingTypeValue?.value) {
-        uni.showToast({
-          title: '请先选择会议类型',
-          icon: 'none',
-        })
+    if (hasTaskType(field, MEETING_TASK_TYPE_IDS) && field.alias === 'scorePackage') {
+      if (!requireFieldValue('meetingType', '请先选择会议类型')) {
         return false
         return false
       }
       }
     }
     }
-    if (['34'].includes(field.taskTypeId) && field.alias === 'scorePackage') {
-      const trainingTypeValue = getFieldFormValueByAlias('trainingType')
-      if (!trainingTypeValue?.value) {
-        uni.showToast({
-          title: '请先选择积分包',
-          icon: 'none',
-        })
+
+    if (normalizeTaskTypeId(field.taskTypeId) === '34' && field.alias === 'scorePackage') {
+      const trainingTypeId = requireFieldValue('trainingType', '请先选择培训类型')
+
+      if (!trainingTypeId) {
         return false
         return false
       }
       }
-      const res = await getPackageListByTaskTypeApi(trainingTypeValue.value)
-      setFieldOptions(field, serializeTaskFieldOptions(res.data))
+
+      const loaded = await loadLatestFieldOptions(field, async () => {
+        const res = await getPackageListByTaskTypeApi(trainingTypeId)
+
+        return serializeTaskFieldOptions(res.data)
+      })
+
+      if (!loaded) return false
     }
     }
-    if (['14', '31', '37'].includes(field.taskTypeId) && field.alias === 'surveyTarget') {
-      const realTaskTypeIdValue = getFieldFormValueByAlias('realTaskTypeId')
-      console.log('realTaskTypeIdValue', realTaskTypeIdValue)
 
 
-      if (!realTaskTypeIdValue?.value) {
-        uni.showToast({
-          title: '请先选择调查类别',
-          icon: 'none',
-        })
+    if (hasTaskType(field, SURVEY_TARGET_TASK_TYPE_IDS) && field.alias === 'surveyTarget') {
+      if (!requireFieldValue('realTaskTypeId', '请先选择调查类别')) {
         return false
         return false
       }
       }
-      uni.navigateTo({
-        url: `/pages-common/entity-selector/index?alias=surveyTarget`,
-        events: {
-          surveyTargetSelect: (selectedItem: SurveyTargetItem) => {
-            const name = selectedItem.yymc || selectedItem.gsmc || selectedItem.pharmacyName || ''
-            setFieldFormValueByAlias('surveyTarget', {
-              label: name,
-              value: name,
-            })
-          },
-        },
-      })
-      return false
+
+      return openSurveyTargetSelector('surveyTarget')
     }
     }
 
 
-    if (['81'].includes(field.taskTypeId) && field.alias === 'hospitalOrPharmacy') {
-      const scorePackageValue = getFieldFormValueByAlias('scorePackage')
-      if (!scorePackageValue?.value) {
-        uni.showToast({
-          title: '请先选择积分包',
-          icon: 'none',
-        })
+    if (normalizeTaskTypeId(field.taskTypeId) === '81' && field.alias === 'hospitalOrPharmacy') {
+      if (!requireFieldValue('scorePackage', '请先选择积分包')) {
         return false
         return false
       }
       }
-      uni.navigateTo({
-        url: `/pages-common/entity-selector/index?alias=surveyTarget`,
-        events: {
-          surveyTargetSelect: (selectedItem: SurveyTargetItem) => {
-            const name = selectedItem.yymc || selectedItem.gsmc || selectedItem.pharmacyName || ''
-            const address = `${selectedItem.province}${selectedItem.city}${selectedItem.area}-${selectedItem.address}`
-
-            setFieldFormValueByAlias('hospitalOrPharmacy', {
-              label: name,
-              value: name,
-            })
-            setFieldFormValueByAlias('address', {
-              label: address,
-              value: address,
-            })
-          },
-        },
-      })
-      return false
+
+      return openSurveyTargetSelector('hospitalOrPharmacy', 'address')
     }
     }
 
 
-    if (['75', '76'].includes(field.taskTypeId) && field.alias === 'hospitalOrPharmacy') {
-      const scorePackageValue = getFieldFormValueByAlias('scorePackage')
-      if (!scorePackageValue?.value) {
-        uni.showToast({
-          title: '请先选择积分包',
-          icon: 'none',
-        })
+    if (
+      hasTaskType(field, HOSPITAL_OR_PHARMACY_TASK_TYPE_IDS) &&
+      field.alias === 'hospitalOrPharmacy'
+    ) {
+      if (!requireFieldValue('scorePackage', '请先选择积分包')) {
         return false
         return false
       }
       }
-      uni.navigateTo({
-        url: `/pages-common/entity-selector/index?alias=surveyTarget`,
-        events: {
-          surveyTargetSelect: (selectedItem: SurveyTargetItem) => {
-            const name = selectedItem.yymc || selectedItem.gsmc || selectedItem.pharmacyName || ''
-            const address = `${selectedItem.province}${selectedItem.city}${selectedItem.area}-${selectedItem.address}`
-
-            setFieldFormValueByAlias('hospitalOrPharmacy', {
-              label: name,
-              value: name,
-            })
-            setFieldFormValueByAlias('hospitalOrPharmacyAddress', {
-              label: address,
-              value: address,
-            })
-          },
-        },
-      })
-      return false
+
+      return openSurveyTargetSelector('hospitalOrPharmacy', 'hospitalOrPharmacyAddress')
     }
     }
 
 
     if (field.taskTypeId === '18' && field.alias === 'pharmacyName') {
     if (field.taskTypeId === '18' && field.alias === 'pharmacyName') {
-      uni.navigateTo({
-        url: `/pages-common/entity-selector/index?alias=pharmacyName`,
-        events: {
-          pharmacyNameSelect: (selectedPharmacyItem: PharmacyItem | string) => {
-            setPharmacyNameSelectEvent(selectedPharmacyItem)
-          },
-        },
-      })
-      return false
+      return openEntitySelector<PharmacyItem | string>(
+        'pharmacyName',
+        'pharmacyNameSelect',
+        setPharmacyNameSelectEvent
+      )
     }
     }
     if (field.taskTypeId === '15' && field.alias === 'yymc') {
     if (field.taskTypeId === '15' && field.alias === 'yymc') {
-      uni.navigateTo({
-        url: `/pages-common/entity-selector/index?alias=yymc`,
-        events: {
-          yymcSelect: (selectedHospitalItem: HospitalItem | string) => {
-            setHospitalNameSelectEvent(selectedHospitalItem)
-          },
-        },
-      })
-      return false
+      return openEntitySelector<HospitalItem | string>(
+        'yymc',
+        'yymcSelect',
+        setHospitalNameSelectEvent
+      )
     }
     }
     if (field.taskTypeId === '16' && field.alias === 'companyName') {
     if (field.taskTypeId === '16' && field.alias === 'companyName') {
-      uni.navigateTo({
-        url: `/pages-common/entity-selector/index?alias=companyName`,
-        events: {
-          companySelect: (selectedCompanyItem: DistributionItem | string) => {
-            setCompanyNameSelectEvent(selectedCompanyItem)
-          },
-        },
-      })
-      return false
+      return openEntitySelector<DistributionItem | string>(
+        'companyName',
+        'companySelect',
+        setCompanyNameSelectEvent
+      )
     }
     }
     if (taskTypeId.value === '47') {
     if (taskTypeId.value === '47') {
-      /**
-       * OTC 推广任务示例。
-       */
-
       if (field.alias === 'drugs') {
       if (field.alias === 'drugs') {
-        const scorePackageValue = getFieldFormValueByAlias('scorePackage')
-        if (!scorePackageValue?.value) {
-          uni.showToast({
-            title: '请先选择积分包',
-            icon: 'none',
-          })
+        const packageId = requireFieldValue('scorePackage', '请先选择积分包')
+
+        if (!packageId) {
           return false
           return false
-        } else {
-          const arr = await getListDrugTable(String(scorePackageValue.value))
-          field.options = arr
-          return true
         }
         }
+
+        return loadLatestFieldOptions(field, () => getListDrugTable(packageId))
       }
       }
       if (field.alias === 'realTaskTypeId') {
       if (field.alias === 'realTaskTypeId') {
         const drugsValue = getFieldFormValueByAlias('drugs')
         const drugsValue = getFieldFormValueByAlias('drugs')
@@ -407,51 +359,35 @@ export const useTaskSelectHandlers = ({
       }
       }
     }
     }
 
 
-    if (['85'].includes(field.taskTypeId) && field.alias === 'hospital') {
-      const scorePackageValue = getFieldFormValueByAlias('scorePackage')
-      if (!scorePackageValue?.value) {
-        uni.showToast({
-          title: '请先选择积分包',
-          icon: 'none',
-        })
+    if (normalizeTaskTypeId(field.taskTypeId) === '85' && field.alias === 'hospital') {
+      if (!requireFieldValue('scorePackage', '请先选择积分包')) {
         return false
         return false
       }
       }
-      uni.navigateTo({
-        url: `/pages-common/entity-selector/index?alias=yymc`,
-        events: {
-          yymcSelect: (selectedHospitalItem: HospitalItem | string) => {
-            if (typeof selectedHospitalItem === 'string') return
-            setFieldFormValueByAlias('hospital', {
-              label: selectedHospitalItem.yymc,
-              value: selectedHospitalItem.yymc,
-            })
-            setFieldFormValueByAlias('level', {
-              label: selectedHospitalItem.yygm ?? '',
-              value: selectedHospitalItem.yygm ?? '',
-            })
-          },
-        },
+
+      return openEntitySelector<HospitalItem | string>('yymc', 'yymcSelect', (selectedItem) => {
+        const hospitalName = typeof selectedItem === 'string' ? selectedItem : selectedItem.yymc
+
+        setFieldTextByAlias('hospital', hospitalName)
+
+        if (typeof selectedItem !== 'string') {
+          setFieldTextByAlias('level', selectedItem.yygm)
+        } else {
+          clearFieldValueByAlias('level')
+        }
       })
       })
-      return false
     }
     }
 
 
     return true
     return true
   }
   }
 
 
-  const getListDrugTable = async (packageId: string) => {
-    try {
-      const res = await getListDrugTableApi(packageId)
-      const arr = res.data.map((item: string) => {
-        return {
-          label: item,
-          value: item,
-        }
-      })
-      return arr
-    } catch (error) {
-      console.error('获取药品列表失败:', error)
-      return []
-    }
+  const getListDrugTable = async (packageId: string): Promise<TaskFieldOption[]> => {
+    const res = await getListDrugTableApi(packageId)
+    const drugList = Array.isArray(res.data) ? res.data : []
+
+    return drugList
+      .map((item) => String(item).trim())
+      .filter(Boolean)
+      .map((item) => ({ label: item, value: item }))
   }
   }
 
 
   /**
   /**
@@ -464,12 +400,32 @@ export const useTaskSelectHandlers = ({
     field: TaskFieldConfigViewItem,
     field: TaskFieldConfigViewItem,
     event: SingleSelectChangeEvent
     event: SingleSelectChangeEvent
   ) => {
   ) => {
+    const fieldTaskTypeId = normalizeTaskTypeId(field.taskTypeId)
+
+    if (field.alias === 'scorePackage') {
+      clearFieldValuesByAlias(PACKAGE_DEPENDENT_FIELD_ALIASES[fieldTaskTypeId] ?? [])
+      invalidateFieldOptionsByAlias('drugs')
+    }
+
+    if (field.alias === 'meetingType' && MEETING_TASK_TYPE_IDS.has(fieldTaskTypeId)) {
+      clearFieldValueByAlias('scorePackage')
+      clearFieldValueByAlias('drugs')
+      invalidateFieldOptionsByAlias('drugs')
+    }
+
+    if (field.alias === 'trainingType' && fieldTaskTypeId === '34') {
+      clearFieldValueByAlias('scorePackage')
+      invalidateFieldOptionsByAlias('scorePackage')
+    }
+
+    if (field.alias === 'realTaskTypeId' && SURVEY_TARGET_TASK_TYPE_IDS.has(fieldTaskTypeId)) {
+      clearFieldValueByAlias('surveyTarget')
+    }
+
     /**
     /**
      * OTC 推广任务的字段联动示例。
      * OTC 推广任务的字段联动示例。
      */
      */
     if (taskTypeId.value === '47' && field.alias === 'scorePackage') {
     if (taskTypeId.value === '47' && field.alias === 'scorePackage') {
-      clearFieldValueByAlias('drugs')
-      clearFieldValueByAlias('realTaskTypeId')
       otcScoreList.value = []
       otcScoreList.value = []
 
 
       const requestVersion = ++otcScoreRequestVersion
       const requestVersion = ++otcScoreRequestVersion
@@ -501,95 +457,43 @@ export const useTaskSelectHandlers = ({
   const handleMultipleSelectBeforeOpen = async (
   const handleMultipleSelectBeforeOpen = async (
     field: TaskFieldConfigViewItem
     field: TaskFieldConfigViewItem
   ): Promise<boolean> => {
   ): Promise<boolean> => {
-    console.log('field', field)
+    if (isMultipleSelectDrugField(field)) {
+      const packageId = requireFieldValue('scorePackage', '请先选择积分包')
 
 
-    if (field.taskTypeId === '75' && field.alias === 'drugs') {
-      const scorePackageValue = getFieldFormValueByAlias('scorePackage')
-      if (!scorePackageValue?.value) {
-        uni.showToast({
-          title: '请先选择积分包',
-          icon: 'none',
-        })
+      if (!packageId) {
         return false
         return false
       }
       }
-      const res = await getListDrugTable(scorePackageValue.value)
 
 
-      setFieldOptions(field, res)
-      return true
+      return loadLatestFieldOptions(field, () => getListDrugTable(packageId))
     }
     }
 
 
-    /**
-     * 特殊多选接口请求写在这里。
-     *
-     * 示例:
-     *
-     * if (
-     *   taskTypeId.value === '47' &&
-     *   field.alias === 'targetUsers'
-     * ) {
-     *   const packageValue =
-     *     getFieldValueByAlias('scorePackage')
-     *
-     *   if (!packageValue) {
-     *     uni.showToast({
-     *       title: '请先选择分数包',
-     *       icon: 'none',
-     *     })
-     *
-     *     return false
-     *   }
-     *
-     *   const res = await getTargetUserListApi({
-     *     packageId: String(packageValue),
-     *   })
-     *
-     *   setFieldOptions(
-     *     field,
-     *     res.data.map((item) => ({
-     *       label: String(item.name),
-     *       value: String(item.id),
-     *     }))
-     *   )
-     * }
-     */
-
     return true
     return true
   }
   }
 
 
   const setPharmacyNameSelectEvent = (selectedPharmacyItem: PharmacyItem | string) => {
   const setPharmacyNameSelectEvent = (selectedPharmacyItem: PharmacyItem | string) => {
     if (typeof selectedPharmacyItem === 'string') {
     if (typeof selectedPharmacyItem === 'string') {
-      setFieldFormValueByAlias('pharmacyName', {
-        label: selectedPharmacyItem,
-        value: selectedPharmacyItem,
-      })
+      setFieldTextByAlias('pharmacyName', selectedPharmacyItem)
+      clearFieldValuesByAlias(PHARMACY_AUTOFILL_FIELD_ALIASES)
       return
       return
     }
     }
-    setFieldFormValueByAlias('pharmacyName', {
-      label: selectedPharmacyItem.pharmacyName,
-      value: selectedPharmacyItem.pharmacyName,
-    })
-    setFieldFormValueByAlias('pharmacyType', {
-      label: undefined,
-      value: selectedPharmacyItem.pharmacyType,
-    })
-    setFieldFormValueByAlias('address', {
-      label: selectedPharmacyItem.address,
-      value: selectedPharmacyItem.address,
-    })
 
 
-    const address = `${selectedPharmacyItem.province}-${selectedPharmacyItem.city}-${selectedPharmacyItem.area}`
-    setFieldFormValueByAlias('shengshiqu', {
-      label: address,
-      value: address,
-    })
+    setFieldTextByAlias('pharmacyName', selectedPharmacyItem.pharmacyName)
+    setFieldTextByAlias('pharmacyType', selectedPharmacyItem.pharmacyType)
+    setFieldTextByAlias('address', selectedPharmacyItem.address)
+    setFieldTextByAlias(
+      'shengshiqu',
+      formatRegion(
+        selectedPharmacyItem.province,
+        selectedPharmacyItem.city,
+        selectedPharmacyItem.area
+      )
+    )
   }
   }
 
 
   const setHospitalNameSelectEvent = (selectedHospitalItem: HospitalItem | string) => {
   const setHospitalNameSelectEvent = (selectedHospitalItem: HospitalItem | string) => {
     if (typeof selectedHospitalItem === 'string') {
     if (typeof selectedHospitalItem === 'string') {
-      setFieldFormValueByAlias('yymc', {
-        label: selectedHospitalItem,
-        value: selectedHospitalItem,
-      })
+      setFieldTextByAlias('yymc', selectedHospitalItem)
+      clearFieldValuesByAlias(HOSPITAL_AUTOFILL_FIELD_ALIASES)
       return
       return
     }
     }
 
 
@@ -598,44 +502,25 @@ export const useTaskSelectHandlers = ({
       value: selectedHospitalItem.id,
       value: selectedHospitalItem.id,
     })
     })
 
 
-    const city = `${selectedHospitalItem.province}-${selectedHospitalItem.city}-${selectedHospitalItem.area}`
-    setFieldFormValueByAlias('city', {
-      label: city,
-      value: city,
-    })
-    setFieldFormValueByAlias('address', {
-      label: selectedHospitalItem.address,
-      value: selectedHospitalItem.address,
-    })
-    setFieldFormValueByAlias('level', {
-      label: selectedHospitalItem.yygm ?? '',
-      value: undefined,
-    })
-    setFieldFormValueByAlias('ownerType', {
-      label: selectedHospitalItem.yyxz ?? '',
-      value: undefined,
-    })
-    setFieldFormValueByAlias('website', {
-      label: selectedHospitalItem.website ?? '',
-      value: selectedHospitalItem.website ?? '',
-    })
-
-    setFieldFormValueByAlias('bedNumber', {
-      label: selectedHospitalItem.ybczs ?? '',
-      value: undefined,
-    })
-
-    console.log('setHospitalNameSelectEvent', selectedHospitalItem)
+    setFieldTextByAlias(
+      'city',
+      formatRegion(
+        selectedHospitalItem.province,
+        selectedHospitalItem.city,
+        selectedHospitalItem.area
+      )
+    )
+    setFieldTextByAlias('address', selectedHospitalItem.address)
+    setFieldTextByAlias('level', selectedHospitalItem.yygm)
+    setFieldTextByAlias('ownerType', selectedHospitalItem.yyxz)
+    setFieldTextByAlias('website', selectedHospitalItem.website)
+    setFieldTextByAlias('bedNumber', selectedHospitalItem.ybczs)
   }
   }
 
 
   const setCompanyNameSelectEvent = (selectedCompanyItem: DistributionItem | string) => {
   const setCompanyNameSelectEvent = (selectedCompanyItem: DistributionItem | string) => {
-    console.log('selectedCompanyItem', selectedCompanyItem)
-
     if (typeof selectedCompanyItem === 'string') {
     if (typeof selectedCompanyItem === 'string') {
-      setFieldFormValueByAlias('companyName', {
-        label: selectedCompanyItem,
-        value: selectedCompanyItem,
-      })
+      setFieldTextByAlias('companyName', selectedCompanyItem)
+      clearFieldValuesByAlias(COMPANY_AUTOFILL_FIELD_ALIASES)
       return
       return
     }
     }
 
 
@@ -644,50 +529,20 @@ export const useTaskSelectHandlers = ({
       value: selectedCompanyItem.id,
       value: selectedCompanyItem.id,
     })
     })
 
 
-    setFieldFormValueByAlias('address', {
-      label: selectedCompanyItem.sygsdz,
-      value: selectedCompanyItem.sygsdz,
-    })
-    setFieldFormValueByAlias('gsgm', {
-      label: selectedCompanyItem.gsgm,
-      value: undefined,
-    })
-    setFieldFormValueByAlias('shyshxydm', {
-      label: selectedCompanyItem.shyshxydm,
-      value: selectedCompanyItem.shyshxydm,
-    })
-    setFieldFormValueByAlias('shyshxydm', {
-      label: selectedCompanyItem.shyshxydm,
-      value: selectedCompanyItem.shyshxydm,
-    })
-    setFieldFormValueByAlias('zczb', {
-      label: selectedCompanyItem.zczb,
-      value: selectedCompanyItem.zczb,
-    })
-    setFieldFormValueByAlias('frdb', {
-      label: selectedCompanyItem.frdb,
-      value: selectedCompanyItem.frdb,
-    })
+    setFieldTextByAlias('address', selectedCompanyItem.sygsdz)
+    setFieldTextByAlias('gsgm', selectedCompanyItem.gsgm)
+    setFieldTextByAlias('shyshxydm', selectedCompanyItem.shyshxydm)
+    setFieldTextByAlias('zczb', selectedCompanyItem.zczb)
+    setFieldTextByAlias('frdb', selectedCompanyItem.frdb)
     setFieldFormValueByAlias('medicineTraceability', {
     setFieldFormValueByAlias('medicineTraceability', {
-      label: undefined,
+      label:
+        selectedCompanyItem.medicineTraceabilityName ?? selectedCompanyItem.medicineTraceability,
       value: selectedCompanyItem.medicineTraceability,
       value: selectedCompanyItem.medicineTraceability,
     })
     })
-    setFieldFormValueByAlias('medicineTraceabilityOther', {
-      label: selectedCompanyItem.medicineTraceabilityOther,
-      value: selectedCompanyItem.medicineTraceabilityOther,
-    })
-    setFieldFormValueByAlias('gsjj', {
-      label: selectedCompanyItem.gsjj,
-      value: selectedCompanyItem.gsjj,
-    })
-    setFieldFormValueByAlias('jyys', {
-      label: selectedCompanyItem.jyys,
-      value: selectedCompanyItem.jyys,
-    })
-    setFieldFormValueByAlias('registerAddress', {
-      label: selectedCompanyItem.gszcd,
-      value: selectedCompanyItem.gszcd,
-    })
+    setFieldTextByAlias('medicineTraceabilityOther', selectedCompanyItem.medicineTraceabilityOther)
+    setFieldTextByAlias('gsjj', selectedCompanyItem.gsjj)
+    setFieldTextByAlias('jyys', selectedCompanyItem.jyys)
+    setFieldTextByAlias('registerAddress', selectedCompanyItem.gszcd)
   }
   }
   return {
   return {
     handleSingleSelectBeforeOpen,
     handleSingleSelectBeforeOpen,

+ 15 - 4
src/pages-task/task-form/index.vue

@@ -24,7 +24,7 @@
 
 
           <view class="task-form-fields">
           <view class="task-form-fields">
             <template v-for="item in taskFieldConfigList" :key="item.id">
             <template v-for="item in taskFieldConfigList" :key="item.id">
-              <view v-if="form.value[item.id]" class="task-form-field">
+              <view v-if="item.show && form.value[item.id]" class="task-form-field">
                 <SingleSelect
                 <SingleSelect
                   v-if="isSingleSelect(item.taskFiledType)"
                   v-if="isSingleSelect(item.taskFiledType)"
                   v-model:label="form.value[item.id].label"
                   v-model:label="form.value[item.id].label"
@@ -108,7 +108,7 @@
                   :disabled="item.readonly"
                   :disabled="item.readonly"
                 />
                 />
 
 
-                <template v-else-if="isLongText(item.taskFiledType) && item.show">
+                <template v-else-if="isLongText(item.taskFiledType)">
                   <view class="line" />
                   <view class="line" />
 
 
                   <LongText
                   <LongText
@@ -158,6 +158,7 @@
       <button
       <button
         class="task-form-footer__button task-form-footer__button--save"
         class="task-form-footer__button task-form-footer__button--save"
         hover-class="task-form-footer__button--hover"
         hover-class="task-form-footer__button--hover"
+        :disabled="isActionPending"
         @click="handleSave"
         @click="handleSave"
       >
       >
         保存
         保存
@@ -166,6 +167,7 @@
       <button
       <button
         class="task-form-footer__button task-form-footer__button--submit"
         class="task-form-footer__button task-form-footer__button--submit"
         hover-class="task-form-footer__button--hover"
         hover-class="task-form-footer__button--hover"
+        :disabled="isActionPending"
         @click="handleSubmit"
         @click="handleSubmit"
       >
       >
         提交
         提交
@@ -233,6 +235,7 @@ const {
 })
 })
 
 
 const {
 const {
+  isActionPending,
   duplicateImageVisible,
   duplicateImageVisible,
   duplicateImageList,
   duplicateImageList,
   clearDuplicateImages,
   clearDuplicateImages,
@@ -296,7 +299,7 @@ onLoad(async (options: PageLoadOptions = {}) => {
     return
     return
   }
   }
 
 
-  initializeTaskFormPage({
+  void initializeTaskFormPage({
     taskTypeId: taskTypeId.value,
     taskTypeId: taskTypeId.value,
     taskTitle: decodeRouteParam(options.taskTitle),
     taskTitle: decodeRouteParam(options.taskTitle),
   })
   })
@@ -305,7 +308,15 @@ onLoad(async (options: PageLoadOptions = {}) => {
     return
     return
   }
   }
 
 
-  await loadTaskForm(taskTypeId.value)
+  try {
+    await loadTaskForm(taskTypeId.value)
+  } catch (error) {
+    console.error('[task-form] 表单配置加载失败', error)
+    uni.showToast({
+      title: '表单加载失败,请稍后重试',
+      icon: 'none',
+    })
+  }
 })
 })
 </script>
 </script>
 
 

+ 7 - 0
src/pages-task/task-visit/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <view>客户拜访</view>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

+ 7 - 0
src/pages.json

@@ -227,6 +227,13 @@
             "navigationStyle": "default"
             "navigationStyle": "default"
           }
           }
         },
         },
+        {
+          "path": "task-visit/index",
+          "style": {
+            "navigationBarTitleText": "客户拜访",
+            "navigationStyle": "default"
+          }
+        },
         {
         {
           "path": "task-package/detail",
           "path": "task-package/detail",
           "style": {
           "style": {

+ 8 - 0
src/pages/index/index.vue

@@ -154,10 +154,18 @@ const handleOpenStatus = (type: 'on-the-way' | 'wait-approve') => {
 }
 }
 
 
 const handleSelectTask = (item: TASK_TYPE) => {
 const handleSelectTask = (item: TASK_TYPE) => {
+  console.log('handleSelectTask', item)
+
   if (!ensureCanOperate()) return
   if (!ensureCanOperate()) return
   if (item.key === 'customer_visit') {
   if (item.key === 'customer_visit') {
+    uni.navigateTo({
+      url: `/pages-task/task-visit/index`,
+    })
     return
     return
   } else if (item.key === 'info_forward') {
   } else if (item.key === 'info_forward') {
+    uni.switchTab({
+      url: '/pages/information/index',
+    })
     return
     return
   } else {
   } else {
     uni.navigateTo({
     uni.navigateTo({