useTaskFormAction.ts 8.6 KB

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