PageSourceSearch

https://www.meetion.ae/assets/js/unit/form.js

js meetion.ae collected 2026-10-04 20:37:08 UTC 39,871 bytes, 1,041 lines download raw bytes

1// 区号数据:正常页面由 form.html 同步注入 window.countryCallingCodes;
2// 命中旧缓存 HTML(没有数据脚本标签)时,由 ensureCallingCodes() 动态加载自愈
3let _callingCodesPromise = null
4function ensureCallingCodes() {
5  if (window.countryCallingCodes && window.countryCallingCodes.length) return Promise.resolve()
6  if (!_callingCodesPromise) {
7    _callingCodesPromise = new Promise((resolve) => {
8      const ver = document.documentElement.getAttribute('data-app-version')
9      const script = document.createElement('script')
10      script.src = '/assets/js/unit/country-calling-codes.js' + (ver ? '?v=' + ver : '')
11      script.onload = resolve
12      script.onerror = () => resolve() // 加载失败也放行,后续走空数据 +86 兜底
13      document.head.appendChild(script)
14    })
15  }
16  return _callingCodesPromise
17}
18
19// 全局缓存:防止页面多个询盘组件重复请求 IP 信息
20let _ipInfoPromise = null  // 共享的 Promise,确保只发起一次请求
21let _cachedAreaCode = null // 缓存匹配到的电话区号(如 +86)
22class ScriptForm extends Script {
23  constructor(params) {
24    super(params)
25    this.formType = params.formType
26    this.is_inquiry_captcha = Number(params.is_inquiry_captcha) || 0
27  this.$form = document.querySelector(`#inquiry-${this.attrId}`)
28    if (!this.$form) throw new Error('$form not found')
29    this.$submit = this.$form.querySelector('.base-button__inner')
30    this.$file = this.$form.querySelector('[type="file"]')
31    this.$file_box = this.$form.querySelector('.unit-form__control--file')
32    this.$file_list = this.$form.querySelector('.unit-form__file-list')
33    this.file_config = []
34    this.is_record_input = false
35    this.is_record_submit = false
36    this.utils = Utilts.ins()
37
38  }
39  /** 初始化,支持异步 */
40  async init() {
41    this._initTel()
42    this._fix()
43    this._initCheckboxGroup()
44    this._initDropdownList()
45    this._observeFormVisible()
46    this._initInquiry()
47    // console.log('init')
48  }
49  // 使用 IntersectionObserver 懒加载:表单进入视口后才请求 IP 信息
50  _observeFormVisible() {
51    if (!this.$form) return
52    // console.log('form', this.$form)
53    const observer = new IntersectionObserver((entries) => {
54      entries.forEach(entry => {
55        // console.log('entry', entry)
56        if (entry.isIntersecting) {
57          this._getIpInfo()
58          observer.disconnect()
59        }
60      })
61    }, { threshold: 0 })
62    observer.observe(this.$form)
63  }
64  _initTel() {
65    const phoneList = this.$form.querySelectorAll('[name="phone"]')
66    // 存在待初始化的电话框时才确保区号数据可用,避免无电话字段的页面发起数据加载
67    const needInit = Array.from(phoneList).some(dom =>
68      dom.classList.contains('phone-input') && !dom.parentElement?.querySelector('.area-code-display')
69    )
70    if (!needInit) return
71    ensureCallingCodes().then(() => {
72      phoneList.forEach((inputDom, index) => {
73        if (!inputDom.classList.contains('phone-input')) return
74        const has = !!inputDom.parentElement?.querySelector('.area-code-display');
75        if(has) return
76        const displaySelectDom = Object.assign(document.createElement('div'), {
77          className: 'unit-form__control form-control phone-area-code area-code-display dropdown-toggle',
78          textContent: '+86'
79        })
80        const selectDom = Object.assign(document.createElement('select'), {
81          className: 'phone-area-code unit-form__control form-control area-code-select',
82          name: 'phone-area-code'
83        })
84        ;(window.countryCallingCodes || []).forEach(item => {
85          const option = document.createElement('option');
86          option.value = item.code;
87          option.textContent = `(${item.code}) ${window.app.utilts.$t(item.name)}`;
88          selectDom.appendChild(option)
89        })
90        selectDom.addEventListener('change', function(event) {
91          displaySelectDom.innerText = event.target.value
92        })
93        inputDom.classList.add('phone-input')
94        inputDom.before(displaySelectDom, selectDom)
95        // 优先使用已缓存的区号(内存或 sessionStorage),否则默认 +86
96        selectDom.value = _cachedAreaCode || sessionStorage.getItem('areaCode') || '+86'
97        displaySelectDom.textContent = _cachedAreaCode || sessionStorage.getItem('areaCode') || '+86'
98      })
99    })
100  }
101  _fix() {
102    console.log(`this.formType`, this.formType);
103    if (this.formType === 'subscription') return
104    let hasContent = false
105    this.$form.querySelectorAll('textarea').forEach(item => {
106      if (item.getAttribute('name') === 'content') hasContent = true
107    })
108
109    if (!hasContent) {
110      console.log(43, this.$form.querySelector('.unit-form__item.unit-form__item-submit'));
111      this.$form.querySelector('.unit-form__item.unit-form__item-submit').outerHTML = `<div text-style="3" class="follow-font-family_size unit-form__item form-group col-12 ">
112        <label class="unit-form__item-inner ">
113
114          <textarea class="unit-form__control form-control" rows="5" name="content" data-title="content" maxlength="65530" placeholder="content" required="required" minlength="1"></textarea>
115          <div class="invalid-feedback">textarea must not exceed 65530 in length!</div>
116
117          <span class="unit-form__label col-form-label">${this.utils.$t('content')}</span>
118        </label>
119      </div>` + this.$form.querySelector('.unit-form__item.unit-form__item-submit').outerHTML
120    }
121  }
122  _initCheckboxGroup(){
123    this.$form.addEventListener('click', function(evt){
124      if (evt.target.classList.contains('unit-form__checkbox-list-inner')) {
125        evt.stopPropagation()
126        evt.preventDefault()
127        return false
128      }
129      const elem = evt.target.closest('.unit-form__checkbox-inner')
130      if (elem) {
131        evt.stopPropagation()
132        const checkbox = elem.querySelector('input[type="checkbox"]')
133        if(checkbox && evt.target !== checkbox){
134          evt.preventDefault()
135          checkbox.checked = !checkbox.checked
136        }
137      } else if (evt.target.closest('.unit-form__checkbox-list')) {
138        evt.preventDefault()
139      }
140    })
141  }
142
143  _initDropdownList() {
144    var _t = this;
145    const dropdownList = this.$form.querySelectorAll('.dropdown')
146    const showMap = new WeakMap()
147    function showDropdownMenu(dropdownMenu, show) {
148      if (show) {
149        dropdownMenu.classList.add('show')
150        showMap.set(dropdownMenu, true)
151      } else {
152        dropdownMenu.classList.remove('show')
153        showMap.set(dropdownMenu, false)
154      }
155    }
156    document.addEventListener('click', function (evt) {
157      let elem = evt.target.closest('.dropdown')
158      dropdownList.forEach(function (dropdown) {
159        if (dropdown && elem !== dropdown) {
160          const dropdownMenu = dropdown.querySelector('.dropdown-menu')
161          // dropdownMenu.classList.remove('show')
162          showDropdownMenu(dropdownMenu, false)
163        }
164      })
165    })
166    dropdownList.forEach(function (dropdown) {
167      const dropdownToggle = dropdown.querySelector('.dropdown-toggle')
168      const dropdownMenu = dropdown.querySelector('.dropdown-menu')
169      if (dropdownToggle && dropdownMenu) {
170        const input = dropdown.querySelector('input.form-control')
171        const popperInstance = Popper.createPopper(dropdownToggle, dropdownMenu, {
172          placement: 'bottom',
173          strategy: 'absolute',
174          adaptive: false,
175          roundOffsets: false,
176          modifiers: [
177            {
178              name: 'computeStyles',
179              options: {
180                roundOffsets: ({ x, y }) => ({
181                  x: 0,
182                  y: input.parentNode.offsetHeight || 47,
183                }),
184              },
185            },
186          ],
187        })
188        dropdownToggle.addEventListener('click', function () {
189          // dropdownMenu.classList.toggle('show')
190          showDropdownMenu(dropdownMenu, !showMap.get(dropdownMenu)) // 避免移动端 show 添加又移除
191          popperInstance.update() // 触发重新计算定位
192        })
193        dropdownMenu.addEventListener('click', function (evt) {
194          if (evt.target.classList.contains('dropdown-item')) {
195            evt.stopPropagation()
196            evt.preventDefault()
197            if (input&&!(evt.target.classList.contains('multi_select'))) {
198              input.value = evt.target.innerText
199            }
200            // dropdownMenu.classList.remove('show')
201            showDropdownMenu(dropdownMenu, false)
202          }
203          if (evt.target.classList.contains('multi_select')) {
204            evt.stopPropagation()
205            evt.preventDefault()
206            if(evt.target.classList.contains('current_multi_select')){
207              evt.target.classList.remove('current_multi_select')
208            }else{
209              evt.target.classList.add('current_multi_select')
210            }
211            input.style.color = 'transparent'
212            updateTags(dropdownMenu,input)
213          }
214        })
215        var multiListInpur = dropdownMenu.querySelectorAll('input[type="checkbox"]')
216        if(multiListInpur){
217          multiListInpur.forEach((checkbox)=>{
218            checkbox.addEventListener('click', function (evt) {
219              if(checkbox.parentNode.classList.contains('current_multi_select')){
220                checkbox.parentNode.classList.remove('current_multi_select')
221              }else{
222                checkbox.parentNode.classList.add('current_multi_select')
223              }
224              updateTags(dropdownMenu,input)
225            })
226          })
227        }
228        // multiListInpur?.map.addEventListener('click', function (evt) {
229        //   console.log(evt)
230        // })
231      }
232    })
233    function updateTags(dropdown,input) {
234      const $tags_wrap = dropdown.parentNode.querySelector('.multi_select__tags-wrap')
235      const $tags = dropdown.parentNode.querySelector('.multi_select__tags')
236      const $rest = $tags_wrap.querySelector('.multi_select__tags-rest')
237      $rest.classList.add('d-none')
238      let multiList = Array.from(dropdown.querySelectorAll('.multi_select')).forEac
238h(a=>{
239        a.querySelector('input').checked = false;
240      })
241      let list = Array.from(dropdown.querySelectorAll('.current_multi_select'))
242      input.value = ''
243      $tags.innerHTML = list?.map(it => {
244        input.value = input.value + it.title+','
245        it.querySelector('input').checked = true;
246        return `<div class="multi_select__tag">
247          <div class="multi_select__tag-inner">
248          <span class="multi_select__tag-text">${it.title}</span><i class="multi_select__tag-close iconfont icon-action-close-2"></i>
249          </div>
250        </div>`
251      }).join('') || '';
252
253     let tagsSelect =Array.from($tags.querySelectorAll('.multi_select__tag-inner'))
254     tagsSelect.forEach(tag=>{
255
256        var multiSelectClose =  tag.querySelector('.multi_select__tag-close')
257        multiSelectClose.addEventListener('click',function(evt){
258          evt.stopPropagation()
259          evt.preventDefault()
260          let title = tag.querySelector('.multi_select__tag-text').innerText
261          list?.map(it => {
262            if(it.title.trim().toLowerCase() == title.trim().toLowerCase()){
263              it?.click()
264            }
265          })
266        })
267     })
268      // 强制一行 省略后续标签------------------------------------------------------------
269      const $tag_list = $tags.querySelectorAll('.multi_select__tag-inner')
270      const wrapRight =  $tags_wrap.getBoundingClientRect().right
271      let hideCount = 0
272      for (let i = $tag_list.length-1; i >= 1; i--) {
273        const item = $tag_list[i];
274        const rect = item.getBoundingClientRect();
275        if (rect.right > wrapRight) {
276          item.parentNode.classList.add('d-none')
277          hideCount++
278        } else {
279          break
280        }
281      }
282      if (hideCount) {
283        $rest.querySelector('.multi_select__tag-text').innerText = `+ ${hideCount}`
284          for (let i = 1; i < $tag_list.length; i++) { // 检查是否和$rest重叠
285            const item = $tag_list[i];
286            if (!item.parentNode.classList.contains('d-none') && item.getBoundingClientRect().right > $rest.getBoundingClientRect().left - 6) {
287              item.parentNode.classList.add('d-none')
288              hideCount++
289              $rest.querySelector('.multi_select__tag-text').innerText = `+ ${hideCount}`
290              break
291            } else if (item.parentNode.classList.contains('d-none')){
292              break
293            }
294          }
295          $rest.classList.remove('d-none')
296      } else {
297        $rest.classList.add('d-none')
298      }
299      // 标签换行,更新高度 ------------------------------------------------------------
300      // if ($tags_wrap.offsetHeight) {
301      //   $input_wrap.style.height = $tags_wrap.offsetHeight + 8 + 'px'
302      // } else {
303      //   $input_wrap.style.height = ''
304      // }
305      // popperInstance.update()
306    }
307  }
308
309  // 根据访客 IP 获取国家信息,并自动匹配设置电话区号
310  async _getIpInfo() {
311    // 若已有缓存(内存或 sessionStorage),直接应用
312    const cached = _cachedAreaCode || sessionStorage.getItem('areaCode')
313    if (cached) {
314      _cachedAreaCode = cached
315      this._applyAreaCode(cached)
316      return
317    }
318    // 若其他组件正在请求(包括获取IP和获取区号),等待其结果即可
319    if (_ipInfoPromise) {
320      await _ipInfoPromise
321      return
322    }
323    // 将获取IP + 获取区号合并为一个 Promise,防止任何环节重复请求
324    _ipInfoPromise = this._fetchAreaCode()
325    try {
326      const areaCode = await _ipInfoPromise
327      if (areaCode) {
328        _cachedAreaCode = areaCode
329        sessionStorage.setItem('areaCode', areaCode)
330        this._applyAreaCode(areaCode)
331      }
332    } catch (err) {
333      console.error('_getIpInfo error:', err)
334    }
335  }
336
337  // 获取 IP 并请求区号信息,返回匹配到的区号
338  async _fetchAreaCode() {
339    // 参考 setInquiryRecord 中的 IP 获取方式:先读 sessionStorage,没有则请求 ifconfig.me
340    let ip = sessionStorage.getItem('ip')
341    if (!ip) {
342      try {
343        ip = await new Promise((resolve, reject) => {
344          var xhr = new XMLHttpRequest()
345          xhr.timeout = 5000
346          xhr.open('GET', 'https://www.ifconfig.me/ip', true)
347          xhr.onreadystatechange = function () {
348            if (xhr.readyState === 4 && xhr.status === 200) {
349              resolve(xhr.responseText)
350            } else if (xhr.status !== 200) {
351              reject('Error: ' + xhr.status)
352            }
353          }
354          xhr.send()
355        })
356        sessionStorage.setItem('ip', ip)
357      } catch {
358        console.log('ip请求失败')
359        return null
360      }
361    }
362    const res = await this.utilts.request({
363      url: '/visitor/get-ip-info',
364      method: 'GET',
365      params: {
366        ip,
367        lang: 'en'
368      }
369    })
370    if (res.code === 0 && res.data && res.data.countryCode) {
371      await ensureCallingCodes()
372      const codes = window.countryCallingCodes || []
373      const match = codes.find(item =>
374        item.countryCode && item.countryCode.toUpperCase() === res.data.countryCode.toUpperCase()
375      )
376      return match ? match.code : '+86'
377    }
378    return null
379  }
380
381  // 将匹配到的区号应用到页面所有电话区号选择器
382  _applyAreaCode(areaCode) {
383    document.querySelectorAll('.area-code-select').forEach(selectDom => {
384      selectDom.value = areaCode
385      const display = selectDom.parentElement?.querySelector('.area-code-display')
386      if (display) display.textContent = areaCode
387    })
388  }
389
390  /**
391* 初始化询盘
392  */
393  _initInquiry() {
394    window.isInquiry = true
395    this._initEvent()
396    this._submitInquiryForm()
397    this._uploadFileChange()
398  }
399  _initEvent() {
400    // 记录填写记录
401    this.$form.querySelectorAll('input').forEach($el => {
402      $el.addEventListener('focus', () => {
403        if (!this.is_record_input) {
404          this.is_record_input = true
405          this.utilts.setInquiryRecord('inquiry_start').then(res => console.log(res.msg))
406        }
407      })
408    })
409    this.$form.querySelectorAll('input[type="text"],input[type="email"],input[type="tel"]').forEach($el => {
410      $el.addEventListener('input', () => { this.checkText($el) })
411      $el.addEventListener('blur', () => { $el.value = $el.value.trim() })
412    })
413    this.$form.querySelectorAll('textarea').forEach($el => {
414      $el.addEventListener('focus', () => {
415        if (!this.is_record_input) {
416          this.is_record_input = true
417          this.utilts.setInquiryRecord('inquiry_start').then(res => console.log(res.msg))
418        }
419      })
420      $el.addEventListener('input', () => { this.checkText($el) })
421      $el.addEventListener('blur', () => { $el.value = $el.value.trim() })
422    })
423    /** form email 输入的文字转小写 */
424    if (this.$form.querySelectorAll('[type="email"]').length > 0) {
425      this.$form.querySelectorAll('[type="email"]').forEach(formItem => {
426        formItem.addEventListener('blur', function () {
427          this.value = this.value.toLowerCase()
428        })
429      })
430    }
431  }
432
433  // 注册form 提交事件
434  _submitInquiryForm() {
435    this.$form.addEventListener('submit', async (event) => {
436      event.preventDefault()
437      // 验证表单信息是否合法,不合法不提交
438      if (!this._inquirySet(event)) return console.warn('this._inquirySet() return false')
439      this.utilts.setBtnLoadingStatus(this.$submit, true)
440      // 记录提交记录
441      // const data = await this.utilts.setInquiryRecord('inquiry_submit')
442      // 设置,获取要保存的表单信息
443      const { preset_config, custom_config } = this._inquiryConfig()
444      const type = this.utils.urlParams.type
445      switch (this.formType) {
446        // 产品详情询盘
447        case 'product-detail-inquiry': {
448          // 获取其它产品详情信息
449          const inquire_pro = type === 'quoteList' ? this._inquiryQuoteList() : this._inquiryDetailList()
450          // 记录提交记录
451          await this.utilts.setInquiryRecord('inquiry_submit', this._getConfigParams(preset_config, custom_config, inquire_pro))
452          //提交保存表单
453          this._inquiryRequest(preset_config, custom_config, inquire_pro)
454          if(type === 'quoteList') {
455            localStorage.removeItem('productQuoteList')
456            localStorage.removeItem('productQuoteIdList')
457          }
458          break
459        }
460        // 普通询盘
461        case 'inquiry':
462          // 获取其它产品详情信息
463          const inquire_pro = type === 'quoteList' ? this._inquiryQuoteList() : this._inquiryDetailList()
464          // 记录提交记录
465          await this.utilts.setInquiryRecord('inquiry_submit', this._getConfigParams(preset_config, custom_config, inquire_pro))
466          //提交保存表单
467          this._inquiryRequest(preset_config, custom_config, inquire_pro)
468          if(type === 'quoteList') {
469            localStorage.removeItem('productQuoteList')
470            localStorage.removeItem('productQuoteIdList')
471          }
472          break
473        // 社媒询盘
474        case 'socialMedia':
475          // 记录提交记录
476          await this.utilts.setInquiryRecord('inquiry_submit', this._getConfigParams(preset_config, custom_config, {}))
477          //提交保存表单
478          this._inquiryRequest(preset_config, custom_config, {})
479          break
480        // 邮票订阅
481        case 'subscription':
482          await this.utilts.setInquiryRecord('inquiry_submit', this._getConfigParams(preset_config))
483          this._subscriptionsRequest(preset_config)
484          break
485        default:
486          break
487      }
488    })
489  }
490
491  // 检查设置
492  _inquirySet(event) {
493    event.preventDefault()
494    event.stopPropagation()
495    this.is_record_submit = true
496    // 装修模式下不能保存表单
497    if (this.utilts.checkDesign() || !window.isInquiry) return false
498    window.isInquiry = false
499    setTimeout(function () { window.isInquiry = true }, 2000)
500    // this.$form.classList.add('was-validated')
501    let success = true
502    // 验证表单项
503    for (const $formItem of this.$form) {
504      // if (!$formItem.checkValidity()) { success = false; break }
505      let res = this.checkText($formItem)
506      if (!res) { success = false }
507    }
508    const formData = new FormData(this.$form)
509    this.$form.querySelectorAll('.unit-form__checkbox-list[required]').forEach(function(groupNode){
510      const name = groupNode.getAttribute('name')
511      if (name) {
512        const checked = formData.getAll(name)
513        if (checked.length) {
514          groupNode.classList.remove('is-invalid')
515        } else {
516          success = false
517          groupNode.classList.add('is-invalid')
518        }
519      }
520    })
521    this.$form.querySelectorAll('.unit-form__select-box [required]').forEach(function(selectNode){
522      const name = selectNode.getAttribute('name')
523      if (name) {
524        const selectedVal = formData.get(name)
525        if (selectedVal) {
526          selectNode.classList.remove('is-invalid')
527        } else {
528          success = false
529          selectNode.classList.add('is-invalid')
530        }
531      }
532    })
533    // 上传文件验证
534    if (this.$file && !this._fileCheck()) success = false
535    return success
536  }
537  checkText($formItem) {
538    if (!this.is_record_submit) return true
539    let type = $formItem.getAttribute('type')
540    let readonly = $formItem.getAttribute('readonly') !== null
541    if (readonly) return true
542    let checkMap = {  // 校验的输入框类型
543      isText: $formItem.nodeName === 'INPUT' && type === 'text',
544      isEmail: $formItem.nodeName === 'INPUT' && type === 'email',
545      isTel: $formItem.nodeName === 'INPUT' && type === 'tel',
546      isTextarea: $formItem.nodeName === 'TEXTAREA',
547    }
548    let hasValidate = false
549    for (const key in checkMap) {
550      if (checkMap[key]) {
551        hasValidate = true
552        break
553      }
554    }
555    if (!hasValidate) return true
556
557    let required = $formItem.getAttribute('required') !== null
558    let minlength = required ? (parseInt($formItem.getAttribute('minlength')) || 1) : 0
559    let pattern = $formItem.getAttribute('pattern') ? new RegExp($formItem.getAttribute('pattern'), 'ig') : null
560    let value = $formItem.value.toString().trim()
561    let success = true
562
563    if (pattern && !pattern.test(value)) {
564      success = false
565    }
566    if (checkMap['isText'] || checkMap['isTextarea'] || checkMap['isTel']) {
567      if (required && value.length < minlength) {
568        success = false
569      }
570    } else if (checkMap['isEmail'] && !$formItem.checkValidity()) {
571      console.log('isEmail');
572
573      success = false
574    }
575    if (success) {
576      $formItem.classList.remove('is-invalid')
577      $formItem.classList.add('is-valid')
578    } else {
579      $formItem.classList.add('is-invalid')
580      $formItem.classList.remove('is-valid')
581    }
582
583    return success
584  }
585  // 获取config
586  _inquiryConfig() {
587    // 获取要保存的信息
588    var preset_config = {} //默认的表单项
589    var custom_config = [] //自定义的表单项
590    var formControls = this.$form.querySelectorAll('.form-control[name]')
591    /**
592     * @type {{[string]: {custom_config?, multiple_value?}}}
593     */
594    const formControlsMap = Array.from(formControls).reduce(function (map, elem) {
595      const mapKey = elem.getAttribute('name')
596      map[mapKey] = Object.assign({}, elem.dataset)
597      try {
598        if (elem.dataset.custom_config) {
599          map[mapKey].custom_config = JSON.parse(elem.dataset.custom_config)
600        }
601      } catch (e) {
602
603      }
604      return map
605    }, {})
606
607    const _$form = document.querySelector(`#inquiry-${this.attrId}`)
608    const formData = new FormData(_$form)
609    const serializeArray = []
610    const formNames = Array.from(new Set(formData.keys()))
611    for (const fieldName of formNames) {
612      if (formControlsMap[fieldName] && formControlsMap[fieldName].multiple_value && formControlsMap[fieldName].multiple_value !== 'false') {
613        // multiple value
614        serializeArray.push({name: fieldName, value: formData.getAll(fieldName)})
615      } else {
616        // single value
617        serializeArray.push({name: fieldName, value: formData.get(fieldName)})
618      }
619    }
620    // 区分自定义表单元素和系统表单元素
621    serializeArray.forEach(function (item, index) {
622      const formItemConfig = formControlsMap[item.name]
623      const formItemCustomConfig = formItemConfig.custom_config || {}
624      const customFromType = formItemCustomConfig.define_type;
625      if (Array.isArray(item.value)) {
626        if ('multi_image' === customFromType) {
627          item.value = item.value.map(function (val, idx) {
628            try {
629              val = JSON.parse(val)
630            } catch (e) {
631              console.warn(idx, e)
632            }
633            return val
634          })
635          item.value = JSON.stringify(item.value)
636        } else {
637          item.value = item.value.join(',')
638        }
639      }
640      if (item.name.indexOf('form_') === 0) {
641        if (!formItemCustomConfig.define_type) {
642          formItemCustomConfig.define_type = 'text'
643        }
644
645        custom_config.push(Object.assign(formItemCustomConfig, item, {
646          // title: formControls[index].getAttribute('data-title') || formControls[index].placeholder || formControls[index].getAttribute('placeholder')
647          title: formControlsMap[item.name].title || _$form.querySelector(`[name="${item.name}"]`)?.getAttribute('placeholder')
648        }))
649
650      } else if (typeof item.value !== 'object') {
651        preset_config[item.name] = item.value
652      }
653    })
654    // 合并区号和电话号码
655    if(preset_config['phone-area-code'] && preset_config['phone']){
656      preset_config['phone'] = preset_config['phone-area-code'] + ' ' + preset_config['phone']
657    }
658    delete preset_config['phone-area-code']
659    // 如果有上传文件,添加到自定义config里面
660    if (this.file_config.length > 0) {
661      custom_config = [...custom_config, ...this.file_config]
662    }
663
664    return { preset_config, custom_config }
665  }
666
667  // 获取DetailList
668  _inquiryDetailList() {
669    return window.app.info.productInquireList
670      .filter(item => item.selected)
671      .map(item => {
672        return {
673          id: item.content_id,
674          page_url: item.href,
675          thumb: (item.detail_thumb || '').replace(/&amp;/gi, '&'),
676          title: item.detail_title
677        }
678      })  }
679
680  // 获取多产品询盘列表
681  _inquiryQuoteList() {
682    return window.app.info.productQuoteList
683      .map(item => {
684        return {
685          id: item.id,
686          page_url: item.page_url,
687          thumb: item.thumb.replace(/&amp;/gi, '&'),
688          title: item.title,
689          amount: item.amount
690        }
691      })
692  }
693
694  // 询盘表单提交
695  async _inquiryRequest(preset_config, custom_config, inquire_pro) {
696    let params = this._getConfigParams(preset_config, custom_config, inquire_pro)
697    // 产品搭配的询盘只需提交选中的搭配产品
698    let matchProducts = JSON.parse(sessionStorage.getItem('matchProducts') || '[]')
699    let mainProduct = JSON.parse(sessionStorage.getItem('mainProduct'))
700    if (window.app.info.isMatchProducts && matchProducts && matchProducts.length && mainProduct) {
701      params.product_list = matchProducts.map(el => {
702        return {
703          id: el.product_id,
704          page_url: el.page_url,
705          thumb: el.thumb_prefix,
706          title: el.title
707        }
708      })
709      params.product_list.unshift(mainProduct)
710    }
711    const utilts = window.app.utilts
712    const that = this
713    // let flagRetry = false
714    const _handleInquiry = async function (captcha, flagRetry) {
715      if (captcha) {
716        // 腾讯验证
717        const captcha_token = await that.utilts.checkTCaptcha('inquiry')
718        if (captcha_token) params.token = captcha_token
719        else {
720          utilts.setBtnLoadingStatus(that.$submit, false)
721          return
722        }
723      }
724      const stringify = 1
725      const url = '/inquiry/insert'
726      const method = 'POST'
727      if(sessionStorage.getItem('ip')){
728        params.ip = sessionStorage.getItem('ip')
729      }
730      return utilts.request({stringify, url, method, params})
731          .then(res => {
732            utilts.recordClientInquiredTimes()
733            that.handleInquireSuccess(res, params)
734          })
735          .catch((res) => {
736            if (res && res.code && parseInt(res.code) === 103) {
737              // 103 验证码
738              if (flagRetry) {
739                // 防止无限弹验证码
740                throw new Error('retry captcha')
741              }
742              return _handleInquiry(true, true)
743            } else if (res && res.msg) {
744              new Modal().warning(res.msg, 'Prompt').open()
745            } else {
746              new Modal().warning('Unknown error', 'Prompt').open()
747            }
748            utilts.setBtnLoadingStatus(that.$submit, false)
749          })
750    }
751    let needleCaptcha = false
752    const cfgRes = await this.getSiteData()
753    if (cfgRes && cfgRes.data && cfgRes.data.tencent_verify_id) {
754      // 设置最新的验证码客户端id
755      window.app.info.site.tencent_verify_id = cfgRes.data.tencent_verify_id
756    }
757    if (cfgRes.data && cfgRes.data.site_config) {
758      const site_config = cfgRes.data.site_config
759      const tencent_verify_id = cfgRes.data.tencent_verify_id
760      console.log('tencent_verify_id ~~~~~~~~~', tencent_verify_id)
761      const page_type = window.app.info.page.page_type || ''
762      const is_landing_page = page_type === 'landing_page'
763      const captcha_switch = is_landing_page ? site_config.ads_inquiry_captcha_switch : site_config.inquiry_captcha_switch
764      const first_no_check = is_landing_page ? site_config.ads_inquiry_captcha_first_no_check_switch : site_config.inquiry_captcha_first_no_check_switch
765      const count = utilts.getClientInquiredTimes()
766      needleCaptcha = captcha_switch ? ( first_no_check ? count > 0 : true ) : false
767      if (site_config.inquiry_success_redirect_time && !isNaN(site_config.inquiry_success_redirect_time)) {
768        sessionStorage.setItem('success_redirect_time', Number(site_config.inquiry_success_redirect_time))
769      }
770    }
771    return _handleInquiry(needleCaptcha, false)
772  }
773
774  /**
775   * 询盘成功
776   * @param res
777   * @param {{email?:string}} params
778   */
779  handleInquireSuccess(res, params) {
780    this.$form.dispatchEvent(new CustomEvent('form-inquire:success', {id: this.$form.id}))
781    this.utilts.setBtnLoadingStatus(this.$submit, false)
782    this.file_config = []
783    // 社媒询盘记录, 2:表示询盘成功后回到首页时弹出聊天列表
784    if (this.formType === 'socialMedia') this.utilts.setItem('social_status', 2)
785    if (this.formType === 'inquiry' || this.formType === 'product-detail-inquiry') {
786      this.utilts.setGtag('begin_inquiy')
787      const contentId = window.renderInfo && window.renderInfo.page && window.renderInfo.page.content_id || 0
788      this.utilts.recordInquiredProduct(contentId)
789    }
790    sessionStorage.setItem('prev_url', location.pathname)
791    window.app.info.user.guest_id = res.data.member_id
792    sessionStorage.setItem('prev_inquire_email', (params && params.email) || '')
793
794    const type = this.utils.urlParams.type
795    if(type === 'quoteList') {
796      sessionStorage.removeItem('prev_url')
797    }
798    window.location.href = globalThis.Server.getRinseHref('/inquire_success.html', window.app.info.site)
799  }
800
801  /**
802   * 获取是否开启广告页询盘验证
803   * 迁移到 Utilts.getSiteData
804   * @deprecated
805   * @returns {Promise<unknown>}
806   */
807  async getSiteData() {
808    const params = {
809      site_id: window.app.info.site.site_id
810    }
811    return await this.utilts.request({
812      url: '/site/info',
813      method: 'get',
814      params
815    }).then(res => {
816      if (res.code == 0) {
817        if (window.app.info.page.page_type === 'landing_page') {
818          this.is_inquiry_captcha = res.data.ads_inquiry_captcha
819        } else {
820          this.is_inquiry_captcha = res.data.inquiry_captcha
821        }
822      }
823      return res
824    })
825  }
826
827  // 邮件订阅提交
828  async _subscriptionsRequest(config) {
829    const params = {
830      site_id: window.app.info.site.site_id,
831      terminal: window.app.info.client.terminal_type,
832      ip: window.app.info.user.visitor_ip,
833      ...config
834    }
835    // 腾讯验证
836    let captcha_token = await this.utilts.checkTCaptcha('inquiry')
837    if (captcha_token) params.token = captcha_token
838    else {
839      this.utilts.setBtnLoadingStatus(this.$submit, false)
840      return
841    }
842    this.utilts.request({
843      url: '/member/email-subscribe/subscribe',
844      method: 'POST',
845      params
846    })
847      .then(res => {
848        window.dispatchEvent(new CustomEvent('email-subscribe:success', {}))
849        this.utilts.setBtnLoadingStatus(this.$submit, false)
850        new Message().success(this.utilts.$t('The subscription is successful, thank you for your participation'))
851      })
852      .catch(() => {
853        this.utilts.setBtnLoadingStatus(this.$submit, false)
854      })
855  }
856
857  /** 获取表单要提交的参数 */
858  _getConfigParams(preset_config, custom_config, inquire_pro) {
859    let params = Object.assign({}, preset_config, {
860      custom_config: custom_config,
861      site_id: window.app.info.site.site_id,
862      visitor_code: window.app.info.user.visitor_code,
863      visitor_id: window.app.info.user.visitor_id,
864      ip: window.app.info.user.visitor_ip,
865      inquiry_record: this.utilts.getDevice(),
866      // member_id: window.app.info.user.user_id,
867      content_type: window.app.info.source.source_content_type,
868      page_url: window.app.info.tempValidPage.href,
869      page_type: window.app.info.page.page_type,
870      source: window.app.info.source.source_result,
871      referrer: window.app.info.source.referrer || '',
872      language_code: window.app.info.site.language_code
873    })
874    // if (
875    //   window.app.info.checkProductDetail(window.app.info.page.page_type) ||
876    //   window.app.info.checkInquireForm(window.app.info.page.page_type) ||
877    //   window.app.info.checkProductLives(window.app.info.page.page_type)
878    // ) {
879    //   params.product_list = inquire_pro
880    // }
881    params.product_list = inquire_pro
882    if (window.app.info.page.page_type == 'product_detail') {
883      let attrs = ``
884      let pattern_item = document.querySelectorAll('.unit-detail-pattern__items .unit-detail-pattern__item')
885      if (pattern_item && pattern_item.length) {
886        pattern_item.forEach(item => {
887          let pattern_lable = item.querySelector('.unit-detail-pattern__label')
888          let pattern_value = item.querySelector('.unit-detail-pattern__child.active span')
889          if (pattern_value) attrs += `${pattern_lable.innerHTML}: ${pattern_value.innerHTML};`
890        })
891        attrs = attrs.slice(0, attrs.length - 1)
892      }
893      if (params.product_list && params.product_list.length) {
894        params.product_list.forEach(_val => {
895          if (_val.id == window.app.info.productDetail.product_id) {
896            _val.attr = attrs
897          }
898        })
899      }
900    }
901    // 客服询盘参数
902    if (this.formType === 'socialMedia') params.type = 4
903    return params
904  }
905
906  // 绑定上传文件表单事件
907  _uploadFileChange() {
908    if (!this.$file) { return }
909    this.$file.parentNode.parentNode.addEventListener('click', (e) => {
910      if (e.target.tagName.toLowerCase() !== 'input') e.preventDefault()
911    })
912    this.$file.addEventListener('change', async () => {
913      if (!this._fileCheck()) return
914      this._promiseUpload()
915        .then(res => {
916          this._removeLoading()
917        })
918        .catch(err => {
919          this._removeLoading()
920          console.log(err)
921        })
922    })
923  }
924
925  /** 处理异步上传图片 */
926  _promiseUpload() {
927    return new Promise(async (resolve, reject) => {
928      const formName = this.$file.getAttribute('name')
929      const files = this.$file.files
930      if (files.length > 0) {
931        //腾讯验证必须通过
932        // if (!await this.utilts.checkTCaptcha('attach')) {
933        //   reject(new Error('verify is error'))
934        //   this.clearUploadFile()
935        //   return
936        // }
937        this.utilts.setBtnLoadingStatus(this.$submit, true)
938        this.$file.setAttribute('disabled', 'disabled')
939        this._showLoading()
940        // 上传文件
941        this.utilts.uploadFile([...files])
942          .then(fileList => {
943            this.$file.removeAttribute('disabled')
944            let fileArray = []
945            // 处理要提交的格式
946            fileList.forEach((info, index) => {
947              const val = { name: info.name, url: `${info.host}/${info.cname}` }
948              fileArray.push(val)
949              this._setShowImg(info.size, info.name, index)
950            })
951            const _file = { name: formName, title: fileList[0].name, value: JSON.stringify(fileArray) }
952            this.file_config.push(_file)
953            this.$file_box.setAttribute('validated', '')
954            fileArray = []
955            resolve()
956            this.utilts.setBtnLoadingStatus(this.$submit, false)
957          })
958          .catch(err => reject(err))
959      } else {
960        resolve()
961      }
962    })
963  }
964
965  /** 清空文件 */
966  clearUploadFile() {
967    this.$file.value = ''
968    this.uploadCheck = false
969  }
970
971  /** 检查上传图片合法性 */
972  _fileCheck() {
973    //如果有图片列表,上传以后添加到promiseAll里面去,因为要处理异步
974    const _type_ = ['txt', 'doc', 'docx', 'ppt', 'pptx', 'xlsx', 'xls', 'pdf', 'jpg', 'png', 'bmp', 'gif', 'jpeg', 'rar', 'zip', 'mp4']
975    let isTrue = true
976    if (this.$file.hasAttribute('required') && !this.$file.value) isTrue = false
977    Array.from(this.$file.files).forEach(item => {
978      // 限制格式
979      const selfFileType = item.name.split('.')[item.name.split('.').length - 1].toLowerCase()
980      if (isTrue) isTrue = _type_.includes(selfFileType)
981      // 限制大小
982      if (isTrue) isTrue = item.size <= 1024 * 1024 * 20
983      // 文件名不能大于100
984      if (isTrue) isTrue = item.name.length < 100
985    })
986    if (!isTrue) {
987      this.$file.value = ''
988      this.$file_box.classList.add('show_error')
989      return false
990    } else this.$file_box.classList.remove('show_error')
991    return true
992  }
993
994  /** 设置显示图片 */
995  _setShowImg(size, name, index) {
996    // 文件大小
997    if (size < 1024 * 1024) size = (size / 1024).toFixed(2) + 'KB'
998    else size = (size / 1024 / 1024).toFixed(2) + 'MB'
999    // 设置显示的html
1000    const divImg = document.createElement('div')
1001    divImg.className = 'unit-form__file-item'
1002    const formFileIndex = `form_file_${index}`
1003    divImg.id = formFileIndex
1004    let innerHTML = `<div><span class='iconfont icon-wenjian'></span> ${name}</div>`
1005    const span = document.createElement('span')
1006    span.className = 'icon iconfont'
1007    //删除图片
1008    span.addEventListener('click', () => {
1009      this.$file_list.querySelector(`#${formFileIndex}`).remove()
1010      this.file_config.splice(index, 1)
1011      this.$file_box.removeAttribute('validated')
1012      if (this.file_config.length === 0) this.clearUploadFile()
1013    })
1014    divImg.innerHTML = innerHTML
1015    divImg.append(span)
1016    this.$file_list.append(divImg)
1017  }
1018
1019  // 添加上传加载的图片
1020  _showLoading() {
1021    const node = this._getLoadingDiv()
1022    this.$file_list.appendChild(node)
1023  }
1024
1025  //  创建上传提示
1026  _getLoadingDiv() {
1027    const $div = document.createElement('div')
1028    $div.innerHTML = this.utilts.$t('uploading') + '...'
1029    $div.style.cssText = ';color: #908d8d;'
1030    $div.classList.add('div_loading')
1031    $div.width = 30
1032    $div.height = 30
1033    return $div
1034  }
1035
1036  // 删除上传加载的图片
1037  _removeLoading() {
1038    const imgLoading = this.$file_list.querySelector('.div_loading')
1039    if (imgLoading) imgLoading.remove()
1040  }
1041}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.