useTaskFormAction.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import { onScopeDispose, type Ref, ref } from 'vue'
  2. import { draftTaskContentApi, saveTaskContentApi } from '@/services/modules/task/taskFrom'
  3. import type {
  4. SaveTaskContentBody,
  5. SaveTaskContentQuery,
  6. } from '@/services/modules/task/taskFrom/type'
  7. import {
  8. type DynamicFormState,
  9. isMultipleSelectDrugField,
  10. type TaskFieldConfigViewItem,
  11. } from './useTaskForm'
  12. interface UseTaskFormActionOptions {
  13. taskTypeId: Ref<string>
  14. form: Ref<DynamicFormState>
  15. taskFieldConfigList: Ref<TaskFieldConfigViewItem[]>
  16. }
  17. type FormAction = 'save' | 'submit'
  18. const PACKAGE_FIELD_ALIASES = ['scorePackage', 'approvalResult'] as const
  19. const IMAGE_FIELD_TYPE = 'img'
  20. const TEXT_LENGTH_FIELD_TYPES = new Set(['text', 'readonlytext', 'longtext'])
  21. const SUCCESS_TOAST_DURATION = 3000
  22. const API_SUCCESS_CODE = 0
  23. const DUPLICATE_IMAGE_CODE = 1
  24. const ACTION_API_MAP = {
  25. save: draftTaskContentApi,
  26. submit: saveTaskContentApi,
  27. } satisfies Record<FormAction, typeof saveTaskContentApi>
  28. const isEmptyValue = (value: unknown): boolean => {
  29. return value === undefined || value === null || String(value).trim() === ''
  30. }
  31. const normalizeSizeLimit = (value: unknown): number | null => {
  32. if (value === undefined || value === null || value === '') {
  33. return null
  34. }
  35. const size = Number(value)
  36. if (!Number.isFinite(size) || size <= 0) {
  37. return null
  38. }
  39. const normalizedSize = Math.floor(size)
  40. return normalizedSize > 0 ? normalizedSize : null
  41. }
  42. const getContentLength = (value: unknown): number => {
  43. if (value === undefined || value === null) {
  44. return 0
  45. }
  46. return Array.from(String(value).trim()).length
  47. }
  48. const getImageCount = (value: unknown): number => {
  49. if (typeof value !== 'string') {
  50. return 0
  51. }
  52. return value
  53. .split(',')
  54. .map((item) => item.trim())
  55. .filter(Boolean).length
  56. }
  57. const showMessage = (title: string, duration?: number): void => {
  58. uni.showToast({
  59. title,
  60. icon: 'none',
  61. ...(duration === undefined ? {} : { duration }),
  62. })
  63. }
  64. /**
  65. * 从接口 data 中提取有效图片地址。
  66. */
  67. const extractImageUrls = (data: unknown): string[] => {
  68. if (!Array.isArray(data)) {
  69. return []
  70. }
  71. const imageUrls = data
  72. .filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
  73. .map((item) => item.trim())
  74. return [...new Set(imageUrls)]
  75. }
  76. const getSuccessScore = (data: unknown): string | null => {
  77. if (!data || typeof data !== 'object' || Array.isArray(data)) {
  78. return null
  79. }
  80. const score = (data as Record<string, unknown>).score
  81. return typeof score === 'string' || typeof score === 'number' ? String(score) : null
  82. }
  83. export const useTaskFormAction = ({
  84. taskTypeId,
  85. form,
  86. taskFieldConfigList,
  87. }: UseTaskFormActionOptions) => {
  88. const isActionPending = ref(false)
  89. const duplicateImageVisible = ref(false)
  90. const duplicateImageList = ref<string[]>([])
  91. let navigateBackTimer: ReturnType<typeof setTimeout> | null = null
  92. let disposed = false
  93. onScopeDispose(() => {
  94. disposed = true
  95. if (navigateBackTimer) {
  96. clearTimeout(navigateBackTimer)
  97. navigateBackTimer = null
  98. }
  99. })
  100. const findPackageField = (): TaskFieldConfigViewItem | undefined => {
  101. for (const alias of PACKAGE_FIELD_ALIASES) {
  102. const field = taskFieldConfigList.value.find((item) => item.alias === alias)
  103. if (field) return field
  104. }
  105. return undefined
  106. }
  107. const getPackageId = (): string => {
  108. const packageField = findPackageField()
  109. if (!packageField) {
  110. return ''
  111. }
  112. return String(form.value.value[packageField.id]?.value ?? '').trim()
  113. }
  114. const validateImageField = (field: TaskFieldConfigViewItem, value: unknown): boolean => {
  115. const imageCount = getImageCount(value)
  116. const configuredMinSize = normalizeSizeLimit(field.taskFiledMinsize)
  117. const minSize = field.isMustfill === '1' ? (configuredMinSize ?? 1) : configuredMinSize
  118. if (imageCount === 0 && field.isMustfill !== '1') {
  119. return true
  120. }
  121. if (minSize !== null && imageCount < minSize) {
  122. showMessage(`${field.taskFiledValue}最少需要${minSize}张`)
  123. return false
  124. }
  125. return true
  126. }
  127. const validateContentField = (field: TaskFieldConfigViewItem, value: unknown): boolean => {
  128. if (isEmptyValue(value)) {
  129. if (field.isMustfill === '1') {
  130. showMessage(`请完善${field.taskFiledValue}`)
  131. return false
  132. }
  133. return true
  134. }
  135. if (!TEXT_LENGTH_FIELD_TYPES.has(field.taskFiledType)) {
  136. return true
  137. }
  138. const contentLength = getContentLength(value)
  139. const minSize = normalizeSizeLimit(field.taskFiledMinsize)
  140. const maxSize = normalizeSizeLimit(field.taskFiledMaxsize)
  141. if (minSize !== null && contentLength < minSize) {
  142. showMessage(`${field.taskFiledValue}最少输入${minSize}个字符`)
  143. return false
  144. }
  145. if (maxSize !== null && contentLength > maxSize) {
  146. showMessage(`${field.taskFiledValue}最多输入${maxSize}个字符`)
  147. return false
  148. }
  149. return true
  150. }
  151. const validateField = (field: TaskFieldConfigViewItem): boolean => {
  152. const value = form.value.value[field.id]?.value
  153. if (field.taskFiledType === IMAGE_FIELD_TYPE) {
  154. return validateImageField(field, value)
  155. }
  156. return validateContentField(field, value)
  157. }
  158. const validateSubmitForm = (): boolean => {
  159. for (const field of taskFieldConfigList.value) {
  160. if (!field.show) continue
  161. if (!validateField(field)) {
  162. return false
  163. }
  164. }
  165. return true
  166. }
  167. const buildActionQuery = (action: FormAction): SaveTaskContentQuery | null => {
  168. const normalizedTaskTypeId = taskTypeId.value.trim()
  169. if (!normalizedTaskTypeId) {
  170. showMessage('缺少 taskTypeId')
  171. return null
  172. }
  173. const packageId = getPackageId()
  174. if (action === 'submit' && !packageId) {
  175. if (taskFieldConfigList.value.some(isMultipleSelectDrugField)) {
  176. showMessage('请先选择积分包')
  177. return null
  178. }
  179. const packageField = findPackageField()
  180. showMessage(
  181. packageField ? `请完善${packageField.taskFiledValue}` : '表单配置中未找到关联积分包字段'
  182. )
  183. return null
  184. }
  185. return {
  186. taskTypeId: normalizedTaskTypeId,
  187. packageId,
  188. }
  189. }
  190. const buildSubmitPayload = (): SaveTaskContentBody => {
  191. const payload: SaveTaskContentBody = {}
  192. for (const field of taskFieldConfigList.value) {
  193. if (!field.show) continue
  194. const fieldValue = form.value.value[field.id]
  195. payload[field.taskFiledValue] = {
  196. seq: field.seq ?? 0,
  197. ...(field.isMustfill === '1'
  198. ? {
  199. required: true,
  200. }
  201. : {}),
  202. items: [
  203. {
  204. label: String(fieldValue?.label ?? ''),
  205. type: field.originalTaskFiledType,
  206. value: String(fieldValue?.value ?? ''),
  207. },
  208. ],
  209. }
  210. }
  211. return payload
  212. }
  213. const clearDuplicateImages = (): void => {
  214. duplicateImageVisible.value = false
  215. duplicateImageList.value = []
  216. }
  217. const showDuplicateImages = (images: string[]): void => {
  218. duplicateImageList.value = images
  219. duplicateImageVisible.value = true
  220. }
  221. const handleActionSuccess = (action: FormAction, data: unknown): boolean => {
  222. if (action === 'save') {
  223. showMessage('保存成功', SUCCESS_TOAST_DURATION)
  224. } else {
  225. const score = getSuccessScore(data)
  226. if (score === null) {
  227. showMessage('提交结果数据异常')
  228. return false
  229. }
  230. showMessage(`提交成功,若审核通过可获得${score}积分`, SUCCESS_TOAST_DURATION)
  231. }
  232. if (disposed) return false
  233. navigateBackTimer = setTimeout(() => {
  234. navigateBackTimer = null
  235. if (disposed) return
  236. uni.navigateBack({
  237. complete: () => {
  238. if (!disposed) {
  239. isActionPending.value = false
  240. }
  241. },
  242. })
  243. }, SUCCESS_TOAST_DURATION)
  244. return true
  245. }
  246. const executeAction = async (action: FormAction): Promise<void> => {
  247. if (isActionPending.value) {
  248. return
  249. }
  250. isActionPending.value = true
  251. let navigationScheduled = false
  252. try {
  253. if (action === 'submit' && !validateSubmitForm()) {
  254. return
  255. }
  256. const query = buildActionQuery(action)
  257. if (!query) {
  258. return
  259. }
  260. const payload = buildSubmitPayload()
  261. const requestApi = ACTION_API_MAP[action]
  262. const response = await requestApi(query, payload)
  263. if (disposed) return
  264. if (response.code !== API_SUCCESS_CODE) {
  265. const duplicateImages =
  266. action === 'submit' && response.code === DUPLICATE_IMAGE_CODE
  267. ? extractImageUrls(response.data)
  268. : []
  269. if (duplicateImages.length > 0) {
  270. showDuplicateImages(duplicateImages)
  271. return
  272. }
  273. uni.showModal({
  274. title: '提示',
  275. content: response.msg || '操作失败,请稍后重试',
  276. showCancel: false,
  277. })
  278. return
  279. }
  280. navigationScheduled = handleActionSuccess(action, response.data)
  281. } catch (error: unknown) {
  282. if (!disposed) {
  283. console.error(`[useTaskFormAction] ${action} 失败`, error)
  284. }
  285. } finally {
  286. if (!disposed && !navigationScheduled) {
  287. isActionPending.value = false
  288. }
  289. }
  290. }
  291. const handleSave = (): Promise<void> => {
  292. return executeAction('save')
  293. }
  294. const handleSubmit = (): Promise<void> => {
  295. return executeAction('submit')
  296. }
  297. return {
  298. isActionPending,
  299. duplicateImageVisible,
  300. duplicateImageList,
  301. clearDuplicateImages,
  302. handleSave,
  303. handleSubmit,
  304. }
  305. }