PageSourceSearch

https://wushixian.cc/static/js/modules/share.js?v=v1.6.123-ordinary-image-editor

js wushixian.cc collected 2026-10-10 02:22:16 UTC 8,986 bytes, 219 lines download raw bytes

1import { ShareCode } from '../utils/share_code.js';
2
3export class ShareModule {
4    constructor() {
5        this.modal = document.getElementById('share-modal');
6        this.preview = document.getElementById('card-preview');
7        this.imageEl = document.getElementById('card-image');
8        this.promptEl = document.getElementById('card-prompt-text');
9        this.authorEl = document.getElementById('card-author-name');
10        this.urlEl = document.getElementById('card-url-text');
11        this.qrContainer = document.getElementById('card-qr-code');
12        
13        this.authorInput = document.getElementById('share-author-input');
14        this.downloadBtn = document.getElementById('share-download-btn');
15        this.copyBtn = document.getElementById('share-copy-btn');
16        this.closeBtn = document.getElementById('share-close-btn');
17        
18        this.currentImageId = null;
19        
20        this.init();
21    }
22
23    init() {
24        if (!this.modal) return;
25
26        // 绑定事件
27        this.closeBtn.addEventListener('click', () => this.hide());
28        this.modal.addEventListener('click', (e) => {
29            if (e.target === this.modal) this.hide();
30        });
31
32        this.authorInput.addEventListener('input', (e) => {
33            this.updateAuthorDisplay(e.target.value.trim() || 'Anonymous', this.currentInviteCode);
34        });
35
36        this.downloadBtn.addEventListener('click', () => this.downloadCard());
37        if (this.copyBtn) {
38            this.copyBtn.addEventListener('click', () => this.copyCard());
39        }
40    }
41
42    updateAuthorDisplay(name, inviteCode) {
43        // 保存当前的 inviteCode,以便在 input 事件中使用
44        this.currentInviteCode = inviteCode;
45        
46        this.authorEl.innerHTML = `@${name}`;
47        
48        if (inviteCode) {
49            const codeSpan = document.createElement('span');
50            codeSpan.className = 'text-slate-400 font-normal text-xs ml-2 tracking-wide';
51            codeSpan.textContent = `邀请码: ${inviteCode}`;
52            this.authorEl.appendChild(codeSpan);
53        }
54    }
55
56    open(imageUrl, prompt, imageId, username, referenceImages = [], inviteCode = null) {
57        this.currentImageId = imageId;
58        
59        // 1. 设置内容
60        // 使用代理或直接设置 src (注意跨域问题,html2canvas 需要 useCORS: true 且服务器支持)
61        // 这里假设 imageUrl 是同源的或者支持 CORS
62        this.imageEl.crossOrigin = "anonymous"; 
63        this.imageEl.src = imageUrl;
64        
65        this.promptEl.textContent = this.truncatePrompt((prompt || 'No prompt provided.').trim(), 150);
66        
67        // 处理参考图显示
68        const refContainer = document.getElementById('card-ref-images');
69        if (refContainer) {
70            if (referenceImages && referenceImages.length > 0) {
71                refContainer.innerHTML = '';
72                referenceImages.forEach(ref => {
73                    const img = document.createElement('img');
74                    img.src = ref;
75                    img.crossOrigin = "anonymous";
76                    img.className = 'w-12 h-12 rounded object-cover border border-slate-200 shadow-sm bg-white';
77                    refContainer.appendChild(img);
78                });
79                refContainer.parentElement.classList.remove('hidden');
80            } else {
81                refContainer.parentElement.classList.add('hidden');
82            }
83        }
84        
85        // 设置默认作者名
86        const defaultName = username || 'Architect';
87        this.authorInput.value = defaultName;
88        
89        // 更新作者名显示,包含邀请码
90        this.updateAuthorDisplay(defaultName, inviteCode);
91
92        // 2. 生成二维码 (使用 ShareCode 混淆)
93        this.qrContainer.innerHTML = '';
94        
95        const shareCode = ShareCode.encode(imageId);
96        const shareUrl = `${window.location.origin}/share/${shareCode}`;
97        
98        // 更新显示的链接文本 (去掉协议头,保持美观)
99        if (this.urlEl) {
100            this.urlEl.textContent = shareUrl.replace(/^https?:\/\//, '');
101        }
102        
103        new QRCode(this.qrContainer, {
104            text: shareUrl,
105            width: 70, // 对应 CSS 的内部尺寸
106            height: 70,
107            colorDark : "#000000",
108            colorLight : "#ffffff",
109            correctLevel : QRCode.CorrectLevel.L
110        });
111
112        // 3. 显示 Modal
113        this.modal.classList.remove('hidden');
114    }
115
116    hide() {
117        this.modal.classList.add('hidden');
118    }
119
120    truncatePrompt(text, maxLength) {
121        if (!text) return '';
122        if (text.length <= maxLength) return text;
123        return text.slice(0, maxLength) + '...';
124    }
125
126    async copyCard() {
127        if (!this.preview) return;
128
129        const originalBtnText = this.copyBtn.innerHTML;
130        this.copyBtn.disabled = true;
131        this.copyBtn.innerHTML = `<svg class="animate-spin h-5 w-5 text-slate-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><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></svg>`;
132
133        try {
134            // 等待图片加载
135            if (!this.imageEl.complete) {
136                await new Promise((resolve) => {
137                    this.imageEl.onload = resolve;
138                    this.imageEl.onerror = resolve;
139                });
140            }
141
142            const canvas = await html2canvas(this.preview, {
143                useCORS: true,
144                scale: 2,
145                backgroundColor: null,
146                logging: false
147            });
148
149            canvas.toBlob(async (blob) => {
150                try {
151                    await navigator.clipboard.write([
152                        new ClipboardItem({ 'image/png': blob })
153                    ]);
154                    
155                    // 成功反馈
156                    this.copyBtn.innerHTML = `<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
156<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> <span>已复制</span>`;
157                    setTimeout(() => {
158                        this.copyBtn.disabled = false;
159                        this.copyBtn.innerHTML = originalBtnText;
160                    }, 2000);
161                } catch (err) {
162                    console.error('Clipboard write failed:', err);
163                    alert('复制失败,您的浏览器可能不支持此功能');
164                    this.copyBtn.disabled = false;
165                    this.copyBtn.innerHTML = originalBtnText;
166                }
167            });
168
169        } catch (error) {
170            console.error('Card copy generation failed:', error);
171            alert('卡片生成失败,请重试');
172            this.copyBtn.disabled = false;
173            this.copyBtn.innerHTML = originalBtnText;
174        }
175    }
176
177    async downloadCard() {
178        if (!this.preview) return;
179
180        const originalBtnText = this.downloadBtn.innerHTML;
181        this.downloadBtn.disabled = true;
182        this.downloadBtn.innerHTML = `<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><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></svg> 生成中...`;
183
184        try {
185            // 等待图片加载完成(如果是刚设置 src)
186            if (!this.imageEl.complete) {
187                await new Promise((resolve) => {
188                    this.imageEl.onload = resolve;
189                    this.imageEl.onerror = resolve; // 即使失败也继续
190                });
191            }
192
193            const canvas = await html2canvas(this.preview, {
194                useCORS: true,
195                scale: 2, // 高清
196                backgroundColor: null, // 透明背景
197                logging: false
198            });
199
200            // 触发下载
201            const link = document.createElement('a');
202            link.download = `jzai-card-${this.currentImageId || Date.now()}.png`;
203            link.href = canvas.toDataURL('image/png');
204            document.body.appendChild(link);
205            link.click();
206            document.body.removeChild(link);
207            
208            // 可选:关闭弹窗
209            // this.hide();
210
211        } catch (error) {
212            console.error('Card generation failed:', error);
213            alert('卡片生成失败,请重试');
214        } finally {
215            this.downloadBtn.disabled = false;
216            this.downloadBtn.innerHTML = originalBtnText;
217        }
218    }
219}

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.