1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["button", "icon", "text"] 5 static values = { postId: Number, publishingText: String, hashtags: String, i18n: Object, title: String, content: String, shareUrl: String } 6 7 // å¤çå¾®ä¿¡å ¬ä¼å·å¹³å°å享 8 shareToWechatMp(event) { 9 event.preventDefault() 10 11 const button = this.buttonTarget 12 const icon = this.iconTarget 13 const text = this.textTarget 14 15 // 设置å è½½ç¶æ 16 this.setLoadingState(button, text, icon) 17 18 try { 19 // Read full content from data attributes 20 const subject = this.titleValue 21 const rawContent = (this.contentValue || '').trim() 22 const shareUrl = this.shareUrlValue || '' 23 24 if (!subject || !rawContent) { 25 throw new Error(this.getI18nMessage('content_not_found')) 26 } 27 28 // Build body: title + content (no link â WeChat MP doesn't support external links in articles) 29 let body = subject + '\n\n' + rawContent 30 31 // æ¾ç¤ºå¾®ä¿¡å ¬ä¼å·å享模æçªå£ 32 this.showWechatMpModal(subject, body, button, text, icon) 33 34 } catch (error) { 35 console.error('WeChat MP Share Error:', error) 36 this.resetButtonState(button, text, icon) 37 this.showError(error.message || this.getI18nMessage('share_failed')) 38 } 39 } 40 41 // çææ¨èæ ç¾ 42 generateTags() { 43 const tags = [] 44 if (this.hashtagsValue && this.hashtagsValue.trim()) { 45 const existingTags = this.hashtagsValue.split(' ').filter(tag => tag.trim()) 46 tags.push(...existingTags) 47 } 48 return [...new Set(tags)].slice(0, 15) 49 } 50 51 // æ¾ç¤ºå¾®ä¿¡å ¬ä¼å·å享模æçªå£ 52 showWechatMpModal(subject, body, button, text, icon) { 53 const fullContent = body 54 const recommendedTags = this.generateTags() 55 56 // è·åå½é åææ¬ 57 const i18n = this.getI18nTexts() 58 59 // å建模æçªå£ 60 const modal = document.createElement('div') 61 modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50' 62 modal.innerHTML = ` 63 <div class="bg-white rounded-xl p-6 max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto"> 64 <div class="flex items-center justify-between mb-4"> 65 <h3 class="text-lg font-semibold text-ink-900">${i18n.title}</h3> 66 <button id="closeModalBtn" class="text-ink-400 hover:text-ink-500"> 67 <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 68 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> 69 </svg> 70 </button> 71 </div> 72 73 <div class="mb-4"> 74 <label class="block text-sm font-medium text-ink-700 mb-2">${i18n.content_label}ï¼</label> 75 <div class="bg-surface-50 rounded-xl p-4 max-h-32 overflow-y-auto"> 76 <pre class="whitespace-pre-wrap text-sm text-ink-900" id="wechatPreviewContent"></pre> 77 </div> 78 </div> 79 80 ${recommendedTags.length > 0 ? ` 81 <div class="mb-4"> 82 <label class="block text-sm font-medium text-ink-700 mb-2">${i18n.recommended_tags}ï¼</label> 83 <div class="bg-surface-50 rounded-xl p-4"> 84 <div class="flex flex-wrap gap-2"> 85 ${recommendedTags.map(tag => ` 86 <!-- design-system:identity-color å¾®ä¿¡å ¬ä¼å·åç绿,æ ç¾å¾½æ --> 87 <span class="inline-flex items-center px-3 py-1 rounded-full text-sm bg-green-100 text-green-800"> 88 ${tag} 89 </span> 90 `).join('')} 91 </div> 92 </div> 93 </div> 94 ` : ''} 95 96 <div class="mb-6"> 97 <!-- design-system:identity-color å¾®ä¿¡å ¬ä¼å·åç绿,å享æé® --> 98 <button id="shareButton" class="bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-xl w-full font-medium"> 99 <svg class="w-5 h-5 inline mr-2" fill="currentColor" viewBox="0 0 24 24"> 100 <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/> 101 </svg> 102 ${i18n.share_button} 103 </button> 104 </div> 105 106 <!-- design-system:identity-color å¾®ä¿¡å ¬ä¼å·åç绿,æ¥éª¤è¯´ææ¡åº/è¾¹ --> 107 <div class="bg-green-50 border border-green-200 rounded-xl p-4"> 108 <!-- design-system:identity-color å¾®ä¿¡å ¬ä¼å·åç绿,æ¥éª¤è¯´ææ¡æ é¢ --> 109 <h4 class="font-medium text-green-900 mb-2">ð ${i18n.steps}ï¼</h4> 110 <!-- design-system:identity-color å¾®ä¿¡å ¬ä¼å·åç绿,æ¥éª¤è¯´ææ¡æ£æ --> 111 <ol class="text-sm text-green-800 space-y-1"> 112 <li>1. ${i18n.step1}</li> 113 <li>2. ${i18n.step2}</li> 114 <li>3. ${i18n.step3}</li> 115 <li>4. ${i18n.step4}</li> 116 <li>5. ${i18n.step5}</li> 117 </ol> 118 </div> 119 </div> 120 ` 121 122 document.body.appendChild(modal) 123 124 // Set preview content safely via textContent 125 modal.querySelector('#wechatPreviewContent').textContent = fullContent 126 127 // å ³éæé®å¤ç 128 const closeBtn = modal.querySelector('#closeModalBtn') 129 const closeModal = () => { 130 modal.remove() 131 this.resetButtonState(button, text, icon, false) 132 } 133 closeBtn.addEventListener('click', closeModal) 134 135 // æ·»å äºä»¶çå¬å¨ 136 const shareButton = modal.querySelector('#shareButton') 137 138 shareButton.addEventListener('click', () => { 139 // 设置æé®ä¸ºå è½½ç¶æ 140 shareButton.innerHTML = ` 141 <svg class="animate-spin h-5 w-5 inline mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> 142 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> 143 <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> 144 </svg> 145 ${i18n.sharing}... 146 ` 147 shareButton.disabled = true 148 // design-system:identity-color å¾®ä¿¡å ¬ä¼å·åç绿,å享æé®å è½½æ 149 shareButton.className = 'bg-green-400 text-white px-6 py-3 rounded-xl w-full font-medium cursor-wait' 150 151 // Open WeChat MP SYNCHRONOUSLY to avoid popup blocker 152 window.open('https://mp.weixin.qq.com/', '_blank') 153 154 // Build copy content 155 let contentToCopy = fullContent 156 if (recommendedTags.length > 0) { 157 contentToCopy = fullContent + '\n\n' + recommendedTags.join(' ') 158 } 159 160 // Copy to clipboard (async is fine, window already opened) 161 if (navigator.clipboard && window.isSecureContext) { 162 navigator.clipboard.writeText(contentToCopy).then(() => { 163 this.markAsPublished(i18n.share_success) 164 modal.remove() 165 }).catch(error => { 166 console.warn('Failed to copy to clipboard:', error) 167 this.markAsPublished(i18n.share_success) 168 modal.remove() 169 }) 170 } else { 171 this.markAsPublished(i18n.share_success) 172 modal.remove() 173 } 174 }) 175 176 // ç¹å»èæ¯å ³é模æçªå£ 177 modal.addEventListener('click', (e) => { 178 if (e.target === modal) { 179 modal.remove() 180 this.resetButtonState(button, text, icon, false) 181 } 182 }) 183 } 184 185 // 设置å è½½ç¶æ 186 setLoadingState(button, text, icon) { 187 button.disabled = true
188 button.classList.add('opacity-75', 'cursor-not-allowed') 189 button.classList.remove('hover:bg-black') 190 191 // æ¾ç¤ºå è½½å¨ç» 192 icon.innerHTML = ` 193 <svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> 194 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> 195 <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> 196 </svg> 197 ` 198 199 text.textContent = this.publishingTextValue 200 } 201 202 // æ è®°å¸å为已åå¸ 203 async markAsPublished(message) { 204 try { 205 const response = await fetch(`/dashboard/posts/${this.postIdValue}/mark_as_published`, { 206 method: 'PATCH', 207 headers: { 208 'Content-Type': 'application/json', 209 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content') 210 } 211 }) 212 213 if (response.ok) { 214 const data = await response.json() 215 console.log('Post marked as published:', data) 216 this.showSuccess(message || data.message || this.getI18nMessage('marked_as_published')) 217 218 // æ´æ°é¡µé¢ç¶ææ¾ç¤º 219 this.updatePostStatusDisplay() 220 } else { 221 console.error('Failed to mark post as published:', response.status) 222 this.showError(this.getI18nMessage('mark_failed')) 223 } 224 } catch (error) { 225 console.error('Error marking post as published:', error) 226 this.showError(this.getI18nMessage('network_error')) 227 } 228 } 229 230 // æ´æ°é¡µé¢ç¶ææ¾ç¤º 231 updatePostStatusDisplay() { 232 try { 233 // æ¥æ¾å½åå¸åçç¶ææ¾ç¤ºå ç´ 234 const postContainer = this.element.closest('[id^="post_"]') 235 if (!postContainer) return 236 237 const i18n = this.getI18nTexts() 238 239 // æ¥æ¾ç¶ææ¾ç¤ºå ç´ 240 const statusElements = postContainer.querySelectorAll('span') 241 statusElements.forEach(element => { 242 const text = element.textContent.trim() 243 // 认æ è®°,ä¸è®¤æå:æåæ¯ç¿»è¯è¿ç,åªè®¤ãå¾ æå¨åäº«ãæ¶é䏿çé¢åå®å¸ç¶æä¸ä¼å 244 if (element.hasAttribute('data-manual-share-status') || text.includes('å¾ æå¨å享') || text.includes('pending_manual_share')) { 245 // æ´æ°ç¶ææ¾ç¤ºä¸ºå·²åå¸ 246 element.className = 'text-success-600 font-medium' 247 element.textContent = i18n.published 248 } 249 }) 250 251 // æ´æ°æé®ç¶ææ¾ç¤ºä¸ºå·²åå¸ 252 const button = this.buttonTarget 253 const icon = this.iconTarget 254 const text = this.textTarget 255 256 if (button && icon && text) { 257 // ç§»é¤å è½½ç¶æ 258 button.disabled = true 259 button.classList.remove('opacity-75', 'cursor-not-allowed') 260 button.classList.add('bg-ink-900', 'opacity-75') 261 // design-system:identity-color å¾®ä¿¡å ¬ä¼å·åç绿,å享æé®æ è®°å·²å叿¶ç§»é¤åçè² 262 button.classList.remove('bg-green-400', 'hover:bg-green-500') 263 264 // æ´æ°å¾æ 为æå徿 265 icon.innerHTML = ` 266 <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 267 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> 268 </svg> 269 ` 270 271 // æ´æ°ææ¬ 272 text.textContent = i18n.published 273 } 274 275 const path = window.location.pathname 276 if (path.includes('/publish') && !path.includes('/publish/sent')) { 277 setTimeout(() => { 278 postContainer.style.transition = 'opacity 0.5s ease-out, transform 0.5s ease-out' 279 postContainer.style.opacity = '0' 280 postContainer.style.transform = 'translateY(-20px)' 281 282 setTimeout(() => { 283 if (postContainer.parentNode) { 284 postContainer.parentNode.removeChild(postContainer) 285 } 286 }, 500) 287 }, 2000) 288 } 289 290 } catch (error) { 291 console.error('Error updating post status display:', error) 292 } 293 } 294 295 // éç½®æé®ç¶æ 296 resetButtonState(button, text, icon) { 297 const i18n = this.getI18nTexts() 298 button.disabled = false
299 button.classList.remove('opacity-75', 'cursor-not-allowed') 300 button.classList.add('hover:bg-black') 301 302 // æ¢å¤åå§å¾®ä¿¡å ¬ä¼å·å¾æ 303 icon.innerHTML = ` 304 <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24"> 305 <path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.033zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/> 306 </svg> 307 ` 308 309 text.textContent = i18n.publish_now 310 } 311 312 // æ¾ç¤ºæåæ¶æ¯ 313 showSuccess(message) { 314 const toast = this.createToast(message, 'success') 315 document.body.appendChild(toast) 316 317 setTimeout(() => { 318 if (document.body.contains(toast)) { 319 document.body.removeChild(toast) 320 } 321 }, 3000) 322 } 323 324 // æ¾ç¤ºéè¯¯æ¶æ¯ 325 showError(message) { 326 const toast = this.createToast(message, 'error') 327 document.body.appendChild(toast) 328 329 setTimeout(() => { 330 if (document.body.contains(toast)) { 331 document.body.removeChild(toast) 332 } 333 }, 5000) 334 } 335 336 // å建æç¤ºæ¡ 337 createToast(message, type) { 338 const toast = document.createElement('div') 339 toast.className = 'fixed top-4 right-4 max-w-sm w-full z-50 transform transition-all duration-300 ease-in-out' 340 const bgColor = type === 'success' ? 'bg-ink-900' : 'bg-danger-500' 341 const inner = document.createElement('div') 342 inner.className = `${bgColor} text-white px-6 py-4 rounded-xl shadow-lifted` 343 const flex = document.createElement('div') 344 flex.className = 'flex items-center' 345 const span = document.createElement('span') 346 span.className = 'flex-1' 347 span.textContent = message 348 const closeBtn = document.createElement('button') 349 closeBtn.className = 'ml-4 text-white hover:text-surface-200' 350 closeBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>' 351 closeBtn.addEventListener('click', () => toast.remove()) 352 flex.appendChild(span) 353 flex.appendChild(closeBtn) 354 inner.appendChild(flex) 355 toast.appendChild(inner) 356 return toast 357 } 358 359// å½é åææ¬ï¼æ¥èªæå¡ç«¯ i18n valueï¼ 360getI18nTexts() { 361 return this.i18nValue || {} 362} 363 364// å½é åæ¶æ¯ï¼æ¥èªæå¡ç«¯ i18n valueï¼ç¼ºçåéè±æï¼ 365getI18nMessage(key) { 366 const fallback = { 367 network_error: 'Network error. Please try again', 368 share_failed: 'Share failed. Please try again', 369 mark_failed: 'Failed to update status. Please try again', 370 content_not_found: 'Unable to get content', 371 marked_as_published: 'Post marked as published' 372 } 373 return (this.i18nValue && this.i18nValue[key]) || fallback[key] || key 374} 375}
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.