PageSourceSearch

https://autowhisper.xyz/assets/controllers/platform_unbind_controller-c6b78323.js

js autowhisper.xyz collected 2026-10-02 01:12:27 UTC 9,950 bytes, 257 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = ["modal", "message", "confirmButton"]
5  static values = {
6    platformId: Number,
7    platformName: String,
8    unbindUrl: String,
9    messageTemplate: String,
10    i18n: Object
11  }
12
13  connect() {
14    console.log("Platform unbind controller connected")
15    // 第二步之后按钮文案会被服务端那句 warning 改掉,取消/重开时要还原。
16    if (this.hasConfirmButtonTarget) {
17      this.defaultConfirmLabel = this.confirmButtonTarget.textContent.trim()
18    }
19  }
20
21  // 显示解绑确认模态框
22  showModal(event) {
23    event.preventDefault()
24    event.stopPropagation()
25    
26    const button = event.currentTarget
27    const platformId = button.dataset.platformId
28    const platformName = button.dataset.platformName
29    const unbindUrl = button.dataset.unbindUrl
30    
31    console.log("Show unbind modal for:", { platformId, platformName, unbindUrl })
32
33    // ⭐ 每次重新打开都从【第一步】开始:先让服务端说会发生什么,再动手。
34    this.confirmStep = false
35    if (this.hasConfirmButtonTarget && this.defaultConfirmLabel) {
36      this.confirmButtonTarget.textContent = this.defaultConfirmLabel
37    }
38    
39    // 设置当前平台信息
40    this.platformIdValue = parseInt(platformId)
41    this.platformNameValue = platformName
42    this.unbindUrlValue = unbindUrl
43    
44    // 更新确认消息
45    if (this.hasMessageTarget && this.messageTemplateValue) {
46      const message = this.messageTemplateValue.replace('__PLATFORM_NAME__', platformName)
47      this.messageTarget.textContent = message
48    }
49    
50    // 显示模态框
51    if (this.hasModalTarget) {
52      this.modalTarget.classList.remove('hidden')
53    }
54  }
55
56  // 关闭模态框
57  closeModal(event) {
58    if (event) {
59      event.preventDefault()
60      // 只有点击背景时才关闭模态框
61      if (event.target !== event.currentTarget) {
62        return
63      }
64    }
65    
66    if (this.hasModalTarget) {
67      this.modalTarget.classList.add('hidden')
68    }
69  }
70
71  // 停止事件冒泡
72  stopPropagation(event) {
73    event.stopPropagation()
74  }
75
76  // 确认解绑
77  async confirmUnbind(event) {
78    event.preventDefault()
79    
80    if (!this.unbindUrlValue) {
81      console.error('Unbind URL not found')
82      return
83    }
84
85    try {
86      // 获取CSRF token
87      const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
88      
89      // ⛔⛔ 第一次点【不带 confirmed】:先问服务端"按下去会发生什么"。
90      //   有在跑的广告 / 还没发出去的内容时,它会回一句具体的话(以及我们要替他
91      //   做掉的那件事);看完他再点一次才是真的动手。此前这里一上来就带
92      //   confirmed=true ⇒ 服务端那段 warning 永远不会出现 —— 弹窗里只有一句
93      //   通用的"解绑后无法发布内容",而真正的后果(连坐删除)他按下去才知道。
94      const url = new URL(this.unbindUrlValue, window.location.origin)
95      if (this.confirmStep) url.searchParams.set('confirmed', 'true')
96
97      const response = await fetch(url.toString(), {
98        method: 'DELETE',
99        headers: {
100          'Accept': 'application/json',
101          'Content-Type': 'application/json',
102          'X-Requested-With': 'XMLHttpRequest',
103          'X-CSRF-Token': csrfToken
104        }
105      })
106
107      if (response.redirected) {
108        // 如果是重定向响应,直接跳转
109        window.location.href = response.url
110        return
111      }
112
113      // 尝试解析JSON响应
114      let data
115      try {
116        data = await response.json()
117      } catch (jsonError) {
118        // 如果不是JSON响应,可能是HTML重定向
119        if (response.ok) {
120          window.location.reload()
121          return
122        }
123        throw new Error('响应格式错误')
124      }
125      
126      if (data && data.needs_confirmation) {
127        // 服务端把"会发生什么"说清楚了 ⇒ 换成它那句话、把按钮文案也换掉,
128        // 弹窗不关:下一次点击带着 confirmed=true 再发一次。
129        this.confirmStep = true
130        if (this.hasMessageTarget) {
131          this.messageTarget.textContent = data.message
132        }
133        if (this.hasConfirmButtonTarget && data.confirm_label) {
134          this.confirmButtonTarget.textContent = data.confirm_label
135        }
136        return
137      }
138
139      if (data && data.success) {
140        // 关闭模态框
141        this.closeModal()
142        
143        // 显示成功消息并刷新页面
144        const successMessage = this.i18nValue.unbind_success.replace('%{platform}', this.platformNameValue)
145        this.showFlashMessage(data.message || successMessage, 'success')
146        
147        // ⛔⛔ 解绑之后【回他刚才在的那个 tab】。这里曾经写死 'platforms' ——
148        //   那是当年只有发帖渠道能解绑时留下的。广告账户和 WhatsApp 从「投放渠道」
149        //   解绑,却被踢到「发布渠道」(CEO 2026-09-08 真机)。
150        //   ⭐ 回哪儿由服务端答(它知道这个渠道属于哪一块),拿不到才退回当前 URL 的 tab。
151        // ⛔⛔ 算出来的地址【和当前地址一样】时必须走 reload,不能赋 location.href。
152        //   生产实测(CEO 2026-09-08):DELETE 返回 200、行已经删了,而页面上那一行
153        //   纹丝不动 —— 日志里那之后【一次 GET 都没有】。
154        //   当时的地址是 …/workspace?tab=ads#ad-setup(保存广告设置那次 302 带来的
155        //   锚点),data.tab 又正好是 ads ⇒ 目标地址和当前地址一字不差。浏览器把
156        //   「赋一个只有片段(或完全相同)的地址」当作【同文档跳转】:滚一下,不重新加载。
157        //   ⭐ 和平台无关:只要地址上挂着 #,Meta / LinkedIn 一样中招。
158        //   ⚠️ 后果不是"不好看" —— toast 说「已解绑」而那一行还在,客户读到的是
159        //     一句假话,他会再点一次(第二次 404,因为行已经没了)。
160        setTimeout(() => {
161          const target = new URL(window.location.href)
162          const tab = data.tab || target.searchParams.get('tab')
163          if (tab) target.searchParams.set('tab', tab)
164          target.hash = ''
165
166          const current = new URL(window.location.href)
167          current.hash = ''
168
169          if (target.toString() === current.toString()) {
170            window.location.reload()
171          } else {
172            window.location.href = target.toString()
173          }
174        }, 1000)
175      } else {
176        // 显示错误消息
177        const errorMessage = data?.error || this.getI18nMessage('unbind_error')
178        this.showFlashMessage(errorMessage, 'error')
179      }
180    } catch (error) {
181      console.error('解绑平台失败:', error)
182      this.showFlashMessage(this.getI18nMessage('network_error'), 'error')
183    }
184  }
185
186  // 处理键盘事件
187  handleKeydown(event) {
188    if (event.key === 'Escape') {
189      this.closeModal()
190    }
191  }
192
193  // 显示Flash消息 - 统一样式
194  showFlashMessage(message, type) {
195    const container = document.getElementById('toast-container')
196    if (!container) return
197
198    const toast = document.createElement('div')
199    toast.className = `toast ${type === 'success' ? 'bg-ink-900' : 'bg-danger-500'} text-white px-6 py-3 rounded-xl shadow-lifted flex items-center space-x-3 transform transition-all duration-300 opacity-0 translate-x-full w-full`
200    toast.setAttribute('data-controller', 'toast')
201    toast.setAttribute('data-toast-type-value', type === 'success' ? 'success' : 'error')
202
203    const iconSvg = type === 'success'
204      ? `<svg class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>`
205      : `<svg class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>`
206
207    toast.innerHTML = `
208      ${iconSvg}
209      <span class="flex-1 min-w-0">${message}</span>
210      <button data-action="click->toast#close" class="flex-shrink-0 text-white hover:text-surface-200">
211        <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
212          <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
213        </svg>
214      </button>`
215
216    container.appendChild(toast)
217    
218    // 手动触发显示动画和自动消失
219    setTimeout(() => {
220      toast.classList.remove('opacity-0', 'translate-x-full')
221      toast.classList.add('opacity-100', 'translate-x-0')
222    }, 10)
223    
224    // 4秒后自动消失
225    setTimeout(() => {
226      toast.classList.remove('opacity-100', 'translate-x-0')
227      toast.classList.add('opacity-0', 'translate-x-full')
228      
229      // 动画完成后移除元素
230      setTimeout(() => {
231        if (toast.parentNode) {
232          toast.remove()
233        }
234      }, 300)
235    }, 4000)
236    
237    // 手动添加关闭按钮功能
238    const closeBtn = toast.querySelector('[data-action="click->
238toast#close"]')
239    if (closeBtn) {
240      closeBtn.addEventListener('click', () => {
241        toast.classList.remove('opacity-100', 'translate-x-0')
242        toast.classList.add('opacity-0', 'translate-x-full')
243        
244        setTimeout(() => {
245          if (toast.parentNode) {
246            toast.remove()
247          }
248        }, 300)
249      })
250    }
251  }
252  
253  // 获取国际化消息
254  getI18nMessage(key) {
255    return this.i18nValue[key] || 'Message not found'
256  }
257} 

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.