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.