PageSourceSearch

https://autowhisper.xyz/assets/controllers/wechat_mp_share_controller-ea1236d1.js

js autowhisper.xyz collected 2026-10-02 01:13:25 UTC 15,439 bytes, 375 lines download raw bytes

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.