teamBackgroundFile.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. const ALLOWED_EXTENSIONS = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv']
  2. const MAX_FILE_SIZE = 50 * 1024 * 1024
  3. const BACKEND_TIMEZONE_OFFSET_MINUTES = 8 * 60
  4. const SESSION_EXPIRES_AT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})([ T])(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(?:(Z)|([+-])(\d{2}):?(\d{2}))?$/
  5. function validateEnterpriseWebsite(value) {
  6. const normalized = String(value || '').trim()
  7. if (!normalized) return '请输入企业官网,无官网请填写“无”'
  8. if (normalized.length > 500) return '企业官网不能超过500个字符'
  9. if (normalized === '无') return ''
  10. if (!/^https?:\/\/[^/\s?#]+(?:[/?#][^\s]*)?$/i.test(normalized)) return '请输入以http://或https://开头的企业官网'
  11. return ''
  12. }
  13. function validateBackgroundFile(file) {
  14. if (!file || !file.name || !file.size) return '请选择非空文件'
  15. if (file.name.length > 255) return '文件名不能超过255个字符'
  16. const ext = file.name.includes('.') ? file.name.split('.').pop().toLowerCase() : ''
  17. if (!ALLOWED_EXTENSIONS.includes(ext)) return '文件类型不支持'
  18. if (file.size > MAX_FILE_SIZE) return '单个文件不能超过50MB'
  19. return ''
  20. }
  21. function parseSessionExpiresAt(value) {
  22. const matched = SESSION_EXPIRES_AT_PATTERN.exec(String(value || '').trim())
  23. if (!matched) return NaN
  24. const year = Number(matched[1])
  25. const month = Number(matched[2])
  26. const day = Number(matched[3])
  27. const separator = matched[4]
  28. const hour = Number(matched[5])
  29. const minute = Number(matched[6])
  30. const second = Number(matched[7])
  31. const millisecond = Number((matched[8] || '').padEnd(3, '0'))
  32. if (year < 1000 || month < 1 || month > 12 || day < 1 || day > 31
  33. || hour > 23 || minute > 59 || second > 59) return NaN
  34. const hasExplicitTimezone = Boolean(matched[9] || matched[10])
  35. if (separator === 'T' && !hasExplicitTimezone) return NaN
  36. if (separator === ' ' && hasExplicitTimezone) return NaN
  37. const localWallClock = Date.UTC(year, month - 1, day, hour, minute, second, millisecond)
  38. const normalized = new Date(localWallClock)
  39. if (normalized.getUTCFullYear() !== year || normalized.getUTCMonth() !== month - 1
  40. || normalized.getUTCDate() !== day || normalized.getUTCHours() !== hour
  41. || normalized.getUTCMinutes() !== minute || normalized.getUTCSeconds() !== second) return NaN
  42. let offsetMinutes = BACKEND_TIMEZONE_OFFSET_MINUTES
  43. if (matched[9] === 'Z') {
  44. offsetMinutes = 0
  45. } else if (matched[10]) {
  46. const offsetHour = Number(matched[11])
  47. const offsetMinute = Number(matched[12])
  48. if (offsetHour > 23 || offsetMinute > 59) return NaN
  49. offsetMinutes = (offsetHour * 60 + offsetMinute) * (matched[10] === '+' ? 1 : -1)
  50. }
  51. return localWallClock - offsetMinutes * 60 * 1000
  52. }
  53. function getSessionRemainingSeconds(expiresAt, now = Date.now()) {
  54. const expiresAtTime = parseSessionExpiresAt(expiresAt)
  55. if (!Number.isFinite(expiresAtTime)) return 0
  56. return Math.max(0, Math.ceil((expiresAtTime - now) / 1000))
  57. }
  58. function createBackgroundQueueItem(file, id) {
  59. const selectedFile = file || {}
  60. const validationError = validateBackgroundFile(selectedFile)
  61. return {
  62. id,
  63. name: selectedFile.name || '',
  64. path: selectedFile.path || '',
  65. size: selectedFile.size || 0,
  66. progress: 0,
  67. status: validationError ? 'failed' : 'pending',
  68. error: validationError,
  69. validationError
  70. }
  71. }
  72. function getUploadableBackgroundFiles(queue) {
  73. return Array.isArray(queue)
  74. ? queue.filter(file => file.status === 'pending'
  75. || (file.status === 'failed' && !file.validationError))
  76. : []
  77. }
  78. function createTeamSubmitPayload(teamInfo) {
  79. const payload = Object.assign({}, teamInfo || {})
  80. payload.enterpriseWebsite = String(payload.enterpriseWebsite || '').trim()
  81. delete payload.source
  82. delete payload.coachId
  83. delete payload.uploaderId
  84. return payload
  85. }
  86. function sortPayloadValue(value) {
  87. if (Array.isArray(value)) return value.map(sortPayloadValue)
  88. if (!value || typeof value !== 'object') return value
  89. return Object.keys(value).sort().reduce((result, key) => {
  90. result[key] = sortPayloadValue(value[key])
  91. return result
  92. }, {})
  93. }
  94. function createTeamPayloadSnapshot(teamInfo) {
  95. return JSON.stringify(sortPayloadValue(createTeamSubmitPayload(teamInfo)))
  96. }
  97. function createEmptyTextPreviewState() {
  98. return { visible: false, fileName: '', content: '', truncated: false }
  99. }
  100. function createTextPreviewState(result, fallbackFileName) {
  101. const preview = result || {}
  102. return {
  103. visible: true,
  104. fileName: preview.fileName || fallbackFileName || '文本预览',
  105. content: String(preview.content === undefined || preview.content === null ? '' : preview.content),
  106. truncated: Boolean(preview.truncated)
  107. }
  108. }
  109. function createTeamCreationState() {
  110. return {
  111. createdTeamId: '',
  112. submitting: false,
  113. stage: 'idle',
  114. completed: false,
  115. teamPayloadSnapshot: '',
  116. teamQuestionnaireId: '',
  117. publishResult: null,
  118. resumeStage: ''
  119. }
  120. }
  121. function beginTeamSubmission(state) {
  122. if (!state || state.submitting || state.completed) return false
  123. state.submitting = true
  124. return true
  125. }
  126. function rememberCreatedTeam(state, teamId) {
  127. if (!state) return ''
  128. if (!state.createdTeamId && teamId) state.createdTeamId = teamId
  129. return state.createdTeamId
  130. }
  131. function setTeamSubmissionStage(state, stage) {
  132. if (state) state.stage = stage || 'idle'
  133. }
  134. function finishTeamSubmission(state, completed = false) {
  135. if (!state) return
  136. state.submitting = false
  137. state.completed = state.completed || completed
  138. if (completed) state.stage = 'complete'
  139. }
  140. function hasUnresolvedBackgroundFiles(queue) {
  141. return Array.isArray(queue) && queue.some(item => item.status === 'pending'
  142. || item.status === 'uploading'
  143. || (item.status === 'failed' && !item.validationError))
  144. }
  145. module.exports = {
  146. ALLOWED_EXTENSIONS,
  147. MAX_FILE_SIZE,
  148. validateEnterpriseWebsite,
  149. validateBackgroundFile,
  150. parseSessionExpiresAt,
  151. getSessionRemainingSeconds,
  152. createBackgroundQueueItem,
  153. getUploadableBackgroundFiles,
  154. createTeamSubmitPayload,
  155. createTeamPayloadSnapshot,
  156. createEmptyTextPreviewState,
  157. createTextPreviewState,
  158. createTeamCreationState,
  159. beginTeamSubmission,
  160. rememberCreatedTeam,
  161. setTeamSubmissionStage,
  162. finishTeamSubmission,
  163. hasUnresolvedBackgroundFiles
  164. }