teamBackgroundFile.js 5.9 KB

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