| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655 |
- import { ref } from 'vue'
- import dayjs from 'dayjs'
- import { getTaskContentConfigByTaskTypeIdApi } from '@/services/modules/task/taskFrom'
- import type {
- TaskContentConfig,
- TaskContentConfigDeriveFrom,
- TaskFieldConfigItem,
- TaskFieldDictMap,
- TaskTypeItem,
- } from '@/services/modules/task/taskFrom/type'
- import { useUserStore } from '@/stores/modules/user'
- import { normalizeTaskFieldType } from './useTaskFieldType'
- export type FieldValue = string
- /**
- * 日期字段展示格式。
- */
- export const TASK_DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'
- export const HIDE_FIELD_DEPT_ID = ['1611890693', '1611890565']
- /**
- * 这些 taskTypeId 的 datetime 字段不自动初始化当前时间。
- */
- const EXCLUDED_AUTO_DATETIME_BASE_TYPE_IDS = new Set(['81', '85', '86', '87'])
- const ON_SITE_VISIT_TASK_TYPE_ID = '85'
- const ON_SITE_VISIT_TYPE = '现场拜访'
- 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',
- '19',
- '64',
- '75',
- '76',
- '81',
- '85',
- ])
- /**
- * 单选、多选组件统一使用的选项结构。
- *
- * label、value 以及其他扩展属性统一转换为字符串。
- */
- export interface TaskFieldOption {
- label: string
- value: string
- [key: string]: string
- }
- export type TaskFieldConfigViewItem = Omit<TaskFieldConfigItem, 'id'> & {
- id: string
- originalTaskFiledType: string
- show: boolean
- readonly: boolean
- options: TaskFieldOption[] | null
- }
- export interface DynamicFormFieldValue {
- /**
- * 页面展示值。
- *
- * datetime 字段使用 YYYY-MM-DD HH:mm。
- */
- label: FieldValue
- /**
- * 实际提交值。
- *
- * datetime 字段使用 YYYY-MM-DD HH:mm。
- */
- value: FieldValue
- }
- export interface DynamicFormState {
- value: Record<string, DynamicFormFieldValue>
- }
- export interface DerivedFieldChangeEvent {
- previousValue: FieldValue
- value: string
- label: string
- }
- export interface SingleSelectChangeEvent extends DerivedFieldChangeEvent {
- selectedItem: TaskFieldOption
- }
- interface RegisteredTaskForm {
- fields: TaskFieldConfigViewItem[]
- taskTypes: TaskTypeItem[]
- }
- const createEmptyFieldValue = (): DynamicFormFieldValue => ({
- label: '',
- value: '',
- })
- const createEmptyFormState = (): DynamicFormState => ({
- value: {},
- })
- /**
- * 将日期统一截断到分钟。
- *
- * 避免 label 只展示到分钟,但 value 仍包含秒和毫秒。
- */
- const normalizeDateTimeToMinute = (date: ReturnType<typeof dayjs>): ReturnType<typeof dayjs> => {
- return date.second(0).millisecond(0)
- }
- /**
- * 判断当前 datetime 字段是否需要自动初始化当前时间。
- */
- const shouldInitializeCurrentDateTime = (field: TaskFieldConfigViewItem): boolean => {
- const fieldType = String(field.taskFiledType ?? '').trim()
- const taskTypeId = String(field.taskTypeId ?? '').trim()
- return fieldType === 'datetime' && !EXCLUDED_AUTO_DATETIME_BASE_TYPE_IDS.has(taskTypeId)
- }
- const isOnSiteVisitField = (
- field: Pick<TaskFieldConfigItem, 'taskTypeId' | 'alias'>,
- alias: string
- ): boolean => {
- return (
- String(field.taskTypeId).trim() === ON_SITE_VISIT_TASK_TYPE_ID && field.alias?.trim() === alias
- )
- }
- const shouldReadonlyField = (field: TaskFieldConfigItem): boolean => {
- return (
- String(field.taskTypeId).trim() === ON_SITE_VISIT_TASK_TYPE_ID &&
- ON_SITE_VISIT_READONLY_FIELD_ALIASES.has(field.alias?.trim() ?? '')
- )
- }
- 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 => {
- if (isMultipleSelectDrugField(field)) return 'multiple_select'
- return normalizeTaskFieldType(field.taskFiledType)
- }
- /**
- * 创建字段初始值。
- *
- * 符合旧业务规则的 datetime 字段:
- * - label 为 YYYY-MM-DD HH:mm
- * - value 为 YYYY-MM-DD HH:mm
- *
- * 其他字段初始化为空。
- */
- const createInitialFieldValue = (
- field: TaskFieldConfigViewItem,
- initialDateTime: ReturnType<typeof dayjs>
- ): DynamicFormFieldValue => {
- if (isOnSiteVisitField(field, 'visitType')) {
- return {
- label: ON_SITE_VISIT_TYPE,
- value: ON_SITE_VISIT_TYPE,
- }
- }
- if (!shouldInitializeCurrentDateTime(field)) {
- return createEmptyFieldValue()
- }
- const normalizedDateTime = normalizeDateTimeToMinute(initialDateTime)
- return {
- label: normalizedDateTime.format(TASK_DATE_TIME_FORMAT),
- value: normalizedDateTime.format(TASK_DATE_TIME_FORMAT),
- }
- }
- /**
- * 将字典或特殊接口返回的数据转换为统一选项格式。
- *
- * 可以在页面的 beforeOpen 中复用。
- */
- export const serializeTaskFieldOptions = (source: unknown): TaskFieldOption[] => {
- if (!Array.isArray(source)) {
- return []
- }
- return source.reduce<TaskFieldOption[]>((result, item) => {
- if (!item || typeof item !== 'object') {
- return result
- }
- const rawItem = item as Record<string, unknown>
- const normalizedItem = Object.entries(rawItem).reduce<Record<string, string>>(
- (record, [key, value]) => {
- record[key] = value === undefined || value === null ? '' : String(value)
- return record
- },
- {}
- )
- if (!normalizedItem.label || !normalizedItem.value) {
- return result
- }
- result.push({
- ...normalizedItem,
- label: normalizedItem.label,
- value: normalizedItem.value,
- })
- return result
- }, [])
- }
- /**
- * 获取字段已有的字典内容。
- *
- * 以下情况返回 null:
- * 1. 字段没有 dictGroupName
- * 2. 返回字典中不存在该分组
- * 3. 对应字典不是数组
- * 4. 对应字典没有有效选项
- */
- const getFieldDictOptions = (
- field: TaskFieldConfigItem,
- dict: TaskFieldDictMap
- ): TaskFieldOption[] | null => {
- const dictName = field.dictGroupName?.trim()
- if (!dictName) {
- return null
- }
- const source = dict[dictName]
- if (!Array.isArray(source)) {
- return null
- }
- const options = serializeTaskFieldOptions(source)
- return options.length > 0 ? options : null
- }
- /**
- * 将接口原始字段配置转换成页面字段配置。
- *
- * 不处理任何特殊接口或特殊业务判断。
- */
- const serializeTaskFieldConfig = (
- config: TaskFieldConfigItem[],
- dict: TaskFieldDictMap,
- deptId: string
- ): TaskFieldConfigViewItem[] => {
- return config.map((field) => ({
- ...field,
- id: String(field.id),
- originalTaskFiledType: normalizeTaskFieldType(field.taskFiledType),
- taskFiledType: serializeTaskFieldType(field),
- show: shouldShowField(field, deptId),
- readonly: shouldReadonlyField(field),
- options: getFieldDictOptions(field, dict),
- }))
- }
- const isRecord = (source: unknown): source is Record<string, unknown> => {
- return Boolean(source) && typeof source === 'object' && !Array.isArray(source)
- }
- const isTaskFieldConfigItem = (source: unknown): source is TaskFieldConfigItem => {
- if (!isRecord(source)) return false
- const id = source.id
- return (
- (typeof id === 'string' || typeof id === 'number') &&
- String(id).trim().length > 0 &&
- typeof source.taskTypeId === 'string' &&
- typeof source.taskFiledKey === 'string' &&
- typeof source.taskFiledValue === 'string' &&
- source.taskFiledValue.trim().length > 0 &&
- typeof source.taskFiledType === 'string' &&
- source.taskFiledType.trim().length > 0 &&
- (source.alias === null || typeof source.alias === 'string') &&
- (source.dictGroupName === null || typeof source.dictGroupName === 'string')
- )
- }
- const serializeTaskTypes = (source: unknown): TaskTypeItem[] => {
- if (!Array.isArray(source)) {
- return []
- }
- return source.reduce<TaskTypeItem[]>((result, item) => {
- if (!isRecord(item)) {
- return result
- }
- const id = item.id
- const taskScoreEnd = Number(item.taskScoreEnd)
- if (
- (typeof id === 'string' || typeof id === 'number') &&
- String(id).trim() &&
- Number.isFinite(taskScoreEnd)
- ) {
- result.push({ ...item, id: String(id), taskScoreEnd } as unknown as TaskTypeItem)
- }
- return result
- }, [])
- }
- const serializeTaskContentConfigResponse = (source: unknown): TaskContentConfig | null => {
- if (!isRecord(source)) {
- return null
- }
- if (
- !Array.isArray(source.config) ||
- source.config.length === 0 ||
- !source.config.every(isTaskFieldConfigItem)
- ) {
- return null
- }
- return {
- config: source.config,
- dict: isRecord(source.dict) ? (source.dict as TaskFieldDictMap) : {},
- taskTypes: serializeTaskTypes(source.taskTypes),
- }
- }
- const serializeDeriveFrom = (source: unknown): TaskContentConfigDeriveFrom | null => {
- if (!isRecord(source) || typeof source.field !== 'string' || !isRecord(source.valueMapping)) {
- return null
- }
- const field = source.field.trim()
- const valueMapping = Object.entries(source.valueMapping).reduce<Record<string, string>>(
- (result, [value, taskTypeId]) => {
- const normalizedTaskTypeId =
- typeof taskTypeId === 'string' || typeof taskTypeId === 'number'
- ? String(taskTypeId).trim()
- : ''
- if (normalizedTaskTypeId) {
- result[String(value)] = normalizedTaskTypeId
- }
- return result
- },
- {}
- )
- if (!field || !Object.keys(valueMapping).length) {
- return null
- }
- return {
- field,
- valueMapping,
- }
- }
- /**
- * 负责:
- * 1. 获取动态表单配置
- * 2. 将字段 id 转换为字符串
- * 3. 挂载已有字典 options
- * 4. 初始化动态表单值
- * 5. 自动初始化符合条件的 datetime 字段
- *
- * 特殊字段的接口请求不在这里处理,
- * 由页面的 beforeOpen 自行请求并覆盖 field.options。
- */
- export const useTaskForm = () => {
- const userStore = useUserStore()
- const form = ref<DynamicFormState>(createEmptyFormState())
- const taskFieldConfigList = ref<TaskFieldConfigViewItem[]>([])
- const activeTaskTypeId = ref('')
- const taskTypes = ref<TaskTypeItem[]>([])
- const taskFormConfigMap = new Map<string, RegisteredTaskForm>()
- let deriveFrom: TaskContentConfigDeriveFrom | null = null
- let loadRequestVersion = 0
- const createFormState = (config: TaskFieldConfigViewItem[]): DynamicFormState => {
- const formValue: DynamicFormState['value'] = {}
- /**
- * 同一次初始化使用同一个时间点,
- * 避免多个 datetime 字段出现分钟边界差异。
- */
- const initialDateTime = dayjs()
- for (const field of config) {
- formValue[field.id] = createInitialFieldValue(field, initialDateTime)
- }
- return {
- value: formValue,
- }
- }
- const clearTaskFormState = (): void => {
- form.value = createEmptyFormState()
- taskFieldConfigList.value = []
- activeTaskTypeId.value = ''
- taskTypes.value = []
- taskFormConfigMap.clear()
- deriveFrom = null
- }
- const resetTaskForm = (): void => {
- loadRequestVersion += 1
- clearTaskFormState()
- }
- const activateTaskForm = (
- taskTypeId: string,
- derivedFieldValue?: DynamicFormFieldValue
- ): boolean => {
- const normalizedTaskTypeId = taskTypeId.trim()
- const targetTaskForm = taskFormConfigMap.get(normalizedTaskTypeId)
- if (!targetTaskForm) {
- return false
- }
- const { fields: targetConfig, taskTypes: targetTaskTypes } = targetTaskForm
- /**
- * 每次切换都重新创建目标表单状态:
- * - 不保留当前表单已经输入的内容
- * - 不恢复目标表单上一次输入的内容
- * - datetime 等字段继续使用原有初始化规则
- */
- const targetFormState = createFormState(targetConfig)
- if (deriveFrom && derivedFieldValue) {
- const derivedFieldName = deriveFrom.field
- const targetDerivedField = targetConfig.find(
- (field) => field.taskFiledValue.trim() === derivedFieldName
- )
- if (targetDerivedField) {
- targetFormState.value[targetDerivedField.id] = {
- label: derivedFieldValue.label,
- value: derivedFieldValue.value,
- }
- }
- }
- activeTaskTypeId.value = normalizedTaskTypeId
- taskFieldConfigList.value = targetConfig
- taskTypes.value = targetTaskTypes
- form.value = targetFormState
- return true
- }
- const registerTaskForm = (taskTypeId: string, taskContentConfig: TaskContentConfig): void => {
- const normalizedTaskTypeId = taskTypeId.trim()
- if (!normalizedTaskTypeId) {
- return
- }
- const config = serializeTaskFieldConfig(
- taskContentConfig.config,
- taskContentConfig.dict,
- String(userStore.currentUserInfo?.deptId ?? '')
- )
- const fieldIds = new Set(config.map((field) => field.id))
- const submitKeys = new Set(config.map((field) => field.taskFiledValue))
- if (fieldIds.size !== config.length || submitKeys.size !== config.length) {
- throw new Error(`任务 ${normalizedTaskTypeId} 的字段 ID 或提交键重复`)
- }
- taskFormConfigMap.set(normalizedTaskTypeId, {
- fields: config,
- taskTypes: taskContentConfig.taskTypes ?? [],
- })
- }
- const loadSingleTaskForm = (taskTypeId: string, taskContentConfig: TaskContentConfig): void => {
- registerTaskForm(taskTypeId, taskContentConfig)
- activateTaskForm(taskTypeId)
- }
- const loadMultipleTaskForms = (
- source: Record<string, unknown>,
- requestedTaskTypeId: string
- ): boolean => {
- deriveFrom = serializeDeriveFrom(source.deriveFrom)
- if (!deriveFrom) {
- return false
- }
- for (const [taskTypeId, taskContentConfig] of Object.entries(source)) {
- const serializedTaskContentConfig = serializeTaskContentConfigResponse(taskContentConfig)
- if (taskTypeId !== 'deriveFrom' && serializedTaskContentConfig) {
- registerTaskForm(taskTypeId, serializedTaskContentConfig)
- }
- }
- const mappedTaskTypeIds = Object.values(deriveFrom.valueMapping)
- const initialTaskTypeId =
- (taskFormConfigMap.has(requestedTaskTypeId) ? requestedTaskTypeId : undefined) ??
- mappedTaskTypeIds.find((taskTypeId) =>
- taskFormConfigMap
- .get(taskTypeId)
- ?.fields.some((field) => field.taskFiledValue.trim() === deriveFrom?.field)
- ) ??
- mappedTaskTypeIds.find((taskTypeId) => taskFormConfigMap.has(taskTypeId)) ??
- taskFormConfigMap.keys().next().value
- return typeof initialTaskTypeId === 'string' && activateTaskForm(initialTaskTypeId)
- }
- const restoreDerivedFieldValue = (
- field: TaskFieldConfigViewItem,
- previousValue: FieldValue
- ): void => {
- const currentFieldValue = form.value.value[field.id]
- if (!currentFieldValue) {
- return
- }
- const previousValueString =
- previousValue === undefined || previousValue === '' ? '' : String(previousValue)
- const previousOption = field.options?.find((option) => option.value === previousValueString)
- currentFieldValue.value = previousValueString
- currentFieldValue.label = previousOption?.label ?? ''
- }
- /**
- * 多表单模式下,根据 deriveFrom 指定字段的值切换实际表单。
- *
- * 切换时重新初始化目标表单,仅把派生字段当前选择同步到目标表单。
- */
- const handleDerivedFieldChange = (
- field: TaskFieldConfigViewItem,
- event: DerivedFieldChangeEvent
- ): void => {
- if (!deriveFrom || field.taskFiledValue.trim() !== deriveFrom.field) {
- return
- }
- const targetTaskTypeId = deriveFrom.valueMapping[event.value]
- if (
- !targetTaskTypeId ||
- !activateTaskForm(targetTaskTypeId, {
- label: event.label,
- value: event.value,
- })
- ) {
- restoreDerivedFieldValue(field, event.previousValue)
- console.warn(`[useTaskForm] 未找到派生值 ${event.value} 对应的表单配置`)
- uni.showToast({
- title: '未找到对应的表单配置',
- icon: 'none',
- })
- }
- }
- const loadTaskForm = async (taskTypeId: string): Promise<void> => {
- const normalizedTaskTypeId = taskTypeId.trim()
- if (!normalizedTaskTypeId) {
- throw new Error('缺少 taskTypeId')
- }
- const requestVersion = ++loadRequestVersion
- try {
- const res = await getTaskContentConfigByTaskTypeIdApi(normalizedTaskTypeId)
- if (requestVersion !== loadRequestVersion) {
- return
- }
- const responseData = res.data
- const singleTaskContentConfig = serializeTaskContentConfigResponse(responseData)
- clearTaskFormState()
- if (singleTaskContentConfig) {
- loadSingleTaskForm(normalizedTaskTypeId, singleTaskContentConfig)
- return
- }
- if (!isRecord(responseData) || !loadMultipleTaskForms(responseData, normalizedTaskTypeId)) {
- throw new Error('动态表单配置格式无效')
- }
- } catch (error) {
- if (requestVersion !== loadRequestVersion) {
- return
- }
- clearTaskFormState()
- throw error
- }
- }
- return {
- form,
- taskFieldConfigList,
- activeTaskTypeId,
- taskTypes,
- loadTaskForm,
- resetTaskForm,
- handleDerivedFieldChange,
- }
- }
|