import { onScopeDispose, type Ref, ref } from 'vue' import { draftTaskContentApi, saveTaskContentApi } from '@/services/modules/task/taskFrom' import type { SaveTaskContentBody, SaveTaskContentQuery, } from '@/services/modules/task/taskFrom/type' import { type DynamicFormState, isMultipleSelectDrugField, type TaskFieldConfigViewItem, } from './useTaskForm' interface UseTaskFormActionOptions { taskTypeId: Ref form: Ref taskFieldConfigList: Ref } type FormAction = 'save' | 'submit' const PACKAGE_FIELD_ALIASES = ['scorePackage', 'approvalResult'] as const const IMAGE_FIELD_TYPE = 'img' const TEXT_LENGTH_FIELD_TYPES = new Set(['text', 'readonlytext', 'longtext']) const SUCCESS_TOAST_DURATION = 3000 const API_SUCCESS_CODE = 0 const DUPLICATE_IMAGE_CODE = 1 const ACTION_API_MAP = { save: draftTaskContentApi, submit: saveTaskContentApi, } satisfies Record const isEmptyValue = (value: unknown): boolean => { return value === undefined || value === null || String(value).trim() === '' } const normalizeSizeLimit = (value: unknown): number | null => { if (value === undefined || value === null || value === '') { return null } const size = Number(value) if (!Number.isFinite(size) || size <= 0) { return null } const normalizedSize = Math.floor(size) return normalizedSize > 0 ? normalizedSize : null } const getContentLength = (value: unknown): number => { if (value === undefined || value === null) { return 0 } return Array.from(String(value).trim()).length } const getImageCount = (value: unknown): number => { if (typeof value !== 'string') { return 0 } return value .split(',') .map((item) => item.trim()) .filter(Boolean).length } const showMessage = (title: string, duration?: number): void => { uni.showToast({ title, icon: 'none', ...(duration === undefined ? {} : { duration }), }) } /** * 从接口 data 中提取有效图片地址。 */ const extractImageUrls = (data: unknown): string[] => { if (!Array.isArray(data)) { return [] } const imageUrls = data .filter((item): item is string => typeof item === 'string' && item.trim().length > 0) .map((item) => item.trim()) return [...new Set(imageUrls)] } const getSuccessScore = (data: unknown): string | null => { if (!data || typeof data !== 'object' || Array.isArray(data)) { return null } const score = (data as Record).score return typeof score === 'string' || typeof score === 'number' ? String(score) : null } export const useTaskFormAction = ({ taskTypeId, form, taskFieldConfigList, }: UseTaskFormActionOptions) => { const isActionPending = ref(false) const duplicateImageVisible = ref(false) const duplicateImageList = ref([]) let navigateBackTimer: ReturnType | null = null let disposed = false onScopeDispose(() => { disposed = true if (navigateBackTimer) { clearTimeout(navigateBackTimer) navigateBackTimer = null } }) const findPackageField = (): TaskFieldConfigViewItem | undefined => { 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 packageField = findPackageField() if (!packageField) { return '' } return String(form.value.value[packageField.id]?.value ?? '').trim() } const validateImageField = (field: TaskFieldConfigViewItem, value: unknown): boolean => { const imageCount = getImageCount(value) const configuredMinSize = normalizeSizeLimit(field.taskFiledMinsize) const minSize = field.isMustfill === '1' ? (configuredMinSize ?? 1) : configuredMinSize if (imageCount === 0 && field.isMustfill !== '1') { return true } if (minSize !== null && imageCount < minSize) { showMessage(`${field.taskFiledValue}最少需要${minSize}张`) return false } return true } const validateContentField = (field: TaskFieldConfigViewItem, value: unknown): boolean => { if (isEmptyValue(value)) { if (field.isMustfill === '1') { showMessage(`请完善${field.taskFiledValue}`) return false } return true } if (!TEXT_LENGTH_FIELD_TYPES.has(field.taskFiledType)) { return true } const contentLength = getContentLength(value) const minSize = normalizeSizeLimit(field.taskFiledMinsize) const maxSize = normalizeSizeLimit(field.taskFiledMaxsize) if (minSize !== null && contentLength < minSize) { showMessage(`${field.taskFiledValue}最少输入${minSize}个字符`) return false } if (maxSize !== null && contentLength > maxSize) { showMessage(`${field.taskFiledValue}最多输入${maxSize}个字符`) return false } return true } const validateField = (field: TaskFieldConfigViewItem): boolean => { const value = form.value.value[field.id]?.value if (field.taskFiledType === IMAGE_FIELD_TYPE) { return validateImageField(field, value) } return validateContentField(field, value) } const validateSubmitForm = (): boolean => { for (const field of taskFieldConfigList.value) { if (!field.show) continue if (!validateField(field)) { return false } } return true } const buildActionQuery = (action: FormAction): SaveTaskContentQuery | null => { const normalizedTaskTypeId = taskTypeId.value.trim() if (!normalizedTaskTypeId) { showMessage('缺少 taskTypeId') return null } const packageId = getPackageId() if (action === 'submit' && !packageId) { if (taskFieldConfigList.value.some(isMultipleSelectDrugField)) { showMessage('请先选择积分包') return null } const packageField = findPackageField() showMessage( packageField ? `请完善${packageField.taskFiledValue}` : '表单配置中未找到关联积分包字段' ) return null } return { taskTypeId: normalizedTaskTypeId, packageId, } } const buildSubmitPayload = (): SaveTaskContentBody => { const payload: SaveTaskContentBody = {} for (const field of taskFieldConfigList.value) { if (!field.show) continue const fieldValue = form.value.value[field.id] payload[field.taskFiledValue] = { seq: field.seq ?? 0, ...(field.isMustfill === '1' ? { required: true, } : {}), items: [ { label: String(fieldValue?.label ?? ''), type: field.originalTaskFiledType, value: String(fieldValue?.value ?? ''), }, ], } } return payload } const clearDuplicateImages = (): void => { duplicateImageVisible.value = false duplicateImageList.value = [] } const showDuplicateImages = (images: string[]): void => { duplicateImageList.value = images duplicateImageVisible.value = true } const handleActionSuccess = (action: FormAction, data: unknown): boolean => { if (action === 'save') { showMessage('保存成功', SUCCESS_TOAST_DURATION) } else { const score = getSuccessScore(data) if (score === null) { showMessage('提交结果数据异常') return false } showMessage(`提交成功,若审核通过可获得${score}积分`, SUCCESS_TOAST_DURATION) } if (disposed) return false navigateBackTimer = setTimeout(() => { navigateBackTimer = null if (disposed) return uni.navigateBack({ complete: () => { if (!disposed) { isActionPending.value = false } }, }) }, SUCCESS_TOAST_DURATION) return true } const executeAction = async (action: FormAction): Promise => { if (isActionPending.value) { return } isActionPending.value = true let navigationScheduled = false try { if (action === 'submit' && !validateSubmitForm()) { return } const query = buildActionQuery(action) if (!query) { return } const payload = buildSubmitPayload() const requestApi = ACTION_API_MAP[action] const response = await requestApi(query, payload) if (disposed) return if (response.code !== API_SUCCESS_CODE) { const duplicateImages = action === 'submit' && response.code === DUPLICATE_IMAGE_CODE ? extractImageUrls(response.data) : [] if (duplicateImages.length > 0) { showDuplicateImages(duplicateImages) return } uni.showModal({ title: '提示', content: response.msg || '操作失败,请稍后重试', showCancel: false, }) return } navigationScheduled = handleActionSuccess(action, response.data) } catch (error: unknown) { if (!disposed) { console.error(`[useTaskFormAction] ${action} 失败`, error) } } finally { if (!disposed && !navigationScheduled) { isActionPending.value = false } } } const handleSave = (): Promise => { return executeAction('save') } const handleSubmit = (): Promise => { return executeAction('submit') } return { isActionPending, duplicateImageVisible, duplicateImageList, clearDuplicateImages, handleSave, handleSubmit, } }