@youidian/sdk 3.3.10 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,656 @@
1
+ // src/feedback.ts
2
+ var DEFAULT_CATEGORIES = [
3
+ { value: "BUG", label: "\u95EE\u9898\u53CD\u9988" },
4
+ { value: "PAYMENT", label: "\u652F\u4ED8\u95EE\u9898" },
5
+ { value: "LOGIN", label: "\u767B\u5F55\u95EE\u9898" },
6
+ { value: "SUGGESTION", label: "\u529F\u80FD\u5EFA\u8BAE" },
7
+ { value: "OTHER", label: "\u5176\u4ED6" }
8
+ ];
9
+ var STYLE_ID = "youidian-feedback-widget-style";
10
+ var MAX_IMAGES_PER_MESSAGE = 4;
11
+ var DEFAULT_PANEL_TITLE = "\u53CD\u9988";
12
+ var DEFAULT_PANEL_SUBTITLE = "\u63D0\u4EA4\u95EE\u9898\u3001\u5EFA\u8BAE\u6216\u4F7F\u7528\u53CD\u9988\u3002";
13
+ var DEFAULT_ATTACHMENTS_LABEL = "\u652F\u6301\u7C98\u8D34\u3001\u62D6\u5165\u6216\u4E0A\u4F20\u622A\u56FE\uFF0C\u6700\u591A 4 \u5F20";
14
+ var FEEDBACK_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.6 8.6 0 0 1-7.7 4.7 8.5 8.5 0 0 1-3.8-.9L3 21l1.9-5.2a8.5 8.5 0 0 1-1-4.1 8.6 8.6 0 0 1 8.6-8.6 8.5 8.5 0 0 1 8.5 8.4Z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.4 10.2h7.2M8.4 13.5h4.7" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>`;
15
+ var BUG_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 8.5a4 4 0 0 1 8 0v6a4 4 0 0 1-8 0v-6Z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M5 9h3m8 0h3M5.5 15H8m8 0h2.5M7 5.5 5.5 4M17 5.5 18.5 4M12 4.5V2.8M12 20.5v-12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>`;
16
+ var NOTE_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 3.5h7l3.5 3.5v13.5H7A2.5 2.5 0 0 1 4.5 18V6A2.5 2.5 0 0 1 7 3.5Z" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M14 3.8V7h3.2M8 11h8M8 15h6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>`;
17
+ var MAIL_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 6.5h15v11h-15v-11Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="m5 7 7 6 7-6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
18
+ var IMAGE_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v14H5V5Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="m7.5 16 3.2-3.2 2.4 2.4 1.7-1.7L18 16.8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.8 8.2h.01" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/></svg>`;
19
+ var CHEVRON_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
20
+ function ensureStyle() {
21
+ if (typeof document === "undefined") return;
22
+ if (document.getElementById(STYLE_ID)) return;
23
+ const style = document.createElement("style");
24
+ style.id = STYLE_ID;
25
+ style.textContent = `
26
+ .yd-feedback-root,.yd-feedback-root *{box-sizing:border-box}
27
+ .yd-feedback-root{--yd-feedback-bg:hsl(var(--background,0 0% 100%));--yd-feedback-fg:hsl(var(--foreground,240 10% 3.9%));--yd-feedback-card:hsl(var(--card,0 0% 100%));--yd-feedback-card-fg:hsl(var(--card-foreground,240 10% 3.9%));--yd-feedback-muted:hsl(var(--muted,240 4.8% 95.9%));--yd-feedback-muted-fg:hsl(var(--muted-foreground,240 3.8% 46.1%));--yd-feedback-border:hsl(var(--border,240 5.9% 90%));--yd-feedback-primary:hsl(var(--primary,142 76% 36%));--yd-feedback-primary-fg:hsl(var(--primary-foreground,0 0% 100%));--yd-feedback-ring:hsl(var(--ring,142 76% 36%));font:14px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--yd-feedback-fg)}
28
+ .yd-feedback-root button,.yd-feedback-root input,.yd-feedback-root textarea{font:inherit}
29
+ .yd-feedback-launcher{position:fixed;right:22px;bottom:22px;z-index:2147483000;width:54px;height:54px;display:grid;place-items:center;border:1px solid color-mix(in srgb,var(--yd-feedback-primary) 24%,var(--yd-feedback-border));border-radius:999px;background:var(--yd-feedback-primary);color:var(--yd-feedback-primary-fg);box-shadow:0 18px 45px color-mix(in srgb,var(--yd-feedback-primary) 22%,transparent);cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,background .18s ease}
30
+ .yd-feedback-launcher:hover{transform:translateY(-2px);box-shadow:0 22px 56px color-mix(in srgb,var(--yd-feedback-primary) 30%,transparent)}
31
+ .yd-feedback-launcher:focus-visible{outline:3px solid color-mix(in srgb,var(--yd-feedback-ring) 32%,transparent);outline-offset:3px}
32
+ .yd-feedback-launcher svg{width:27px;height:27px}
33
+ .yd-feedback-panel{position:fixed;inset:auto 22px 86px auto;z-index:2147483000;width:min(760px,calc(100vw - 44px));height:min(560px,calc(100vh - 108px));display:none;grid-template-rows:auto minmax(0,1fr);overflow:hidden;border:1px solid var(--yd-feedback-border);border-radius:18px;background:var(--yd-feedback-card);color:var(--yd-feedback-card-fg);box-shadow:0 24px 70px rgba(15,23,42,.18)}
34
+ .yd-feedback-panel[data-open="true"]{display:grid}
35
+ .yd-feedback-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:18px 20px 16px;border-bottom:1px solid var(--yd-feedback-border);background:var(--yd-feedback-card)}
36
+ .yd-feedback-title{margin:0;font-size:22px;line-height:1.15;font-weight:760;letter-spacing:0;color:var(--yd-feedback-fg)}
37
+ .yd-feedback-subtitle{margin:8px 0 0;color:var(--yd-feedback-muted-fg);font-size:13px;line-height:1.45}
38
+ .yd-feedback-close{display:grid;place-items:center;width:34px;height:34px;border:0;border-radius:10px;background:transparent;color:var(--yd-feedback-muted-fg);cursor:pointer;transition:background .16s ease,color .16s ease}
39
+ .yd-feedback-close:hover{background:var(--yd-feedback-muted);color:var(--yd-feedback-fg)}
40
+ .yd-feedback-close svg{width:24px;height:24px}
41
+ .yd-feedback-shell{display:grid;min-height:0;grid-template-columns:230px minmax(0,1fr)}
42
+ .yd-feedback-sidebar{min-width:0;border-right:1px solid var(--yd-feedback-border);background:var(--yd-feedback-muted)}
43
+ .yd-feedback-sidebar-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 14px;border-bottom:1px solid var(--yd-feedback-border)}
44
+ .yd-feedback-sidebar-title{display:flex;align-items:center;gap:9px;color:var(--yd-feedback-fg);font-size:15px;font-weight:720}
45
+ .yd-feedback-sidebar-title svg{width:20px;height:20px;color:var(--yd-feedback-primary)}
46
+ .yd-feedback-icon-button{display:grid;place-items:center;width:34px;height:34px;border:1px solid var(--yd-feedback-border);border-radius:999px;background:var(--yd-feedback-card);color:var(--yd-feedback-fg);cursor:pointer;transition:background .16s ease,border-color .16s ease}
47
+ .yd-feedback-icon-button:hover{background:color-mix(in srgb,var(--yd-feedback-primary) 8%,var(--yd-feedback-card));border-color:color-mix(in srgb,var(--yd-feedback-primary) 38%,var(--yd-feedback-border))}
48
+ .yd-feedback-icon-button svg{width:18px;height:18px}
49
+ .yd-feedback-thread-list{padding:12px}
50
+ .yd-feedback-thread-card{display:none;border:1px solid var(--yd-feedback-border);border-radius:14px;background:var(--yd-feedback-card);padding:12px}
51
+ .yd-feedback-thread-card[data-visible="true"]{display:block}
52
+ .yd-feedback-thread-badges{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:10px}
53
+ .yd-feedback-badge{display:inline-flex;align-items:center;border:1px solid var(--yd-feedback-border);border-radius:999px;background:var(--yd-feedback-muted);color:var(--yd-feedback-fg);padding:4px 9px;font-size:11px;font-weight:700;line-height:1}
54
+ .yd-feedback-badge[data-status="true"]{border-color:color-mix(in srgb,var(--yd-feedback-primary) 45%,var(--yd-feedback-border));background:color-mix(in srgb,var(--yd-feedback-primary) 12%,var(--yd-feedback-card));color:var(--yd-feedback-primary)}
55
+ .yd-feedback-thread-preview{margin:0;color:var(--yd-feedback-fg);font-size:13px;line-height:1.45;word-break:break-word}
56
+ .yd-feedback-thread-meta{display:flex;justify-content:space-between;gap:10px;margin-top:10px;color:var(--yd-feedback-muted-fg);font-size:11px}
57
+ .yd-feedback-empty-thread{border:1px dashed var(--yd-feedback-border);border-radius:14px;padding:14px;color:var(--yd-feedback-muted-fg);font-size:12px}
58
+ .yd-feedback-main{min-width:0;min-height:0;background:var(--yd-feedback-card);overflow:hidden}
59
+ .yd-feedback-form{height:100%;display:grid;grid-template-rows:auto auto auto minmax(0,1fr) auto auto;gap:14px;padding:16px 18px 0}
60
+ .yd-feedback-row{display:grid;gap:14px;grid-template-columns:180px minmax(0,1fr)}
61
+ .yd-feedback-field{display:flex;min-width:0;flex-direction:column;gap:8px}
62
+ .yd-feedback-label{display:flex;align-items:center;gap:8px;color:var(--yd-feedback-fg);font-size:13px;font-weight:720}
63
+ .yd-feedback-label svg{width:17px;height:17px;color:var(--yd-feedback-fg)}
64
+ .yd-feedback-input,.yd-feedback-textarea{width:100%;border:1px solid var(--yd-feedback-border);border-radius:13px;background:var(--yd-feedback-card);color:var(--yd-feedback-fg);outline:none;transition:border-color .16s ease,box-shadow .16s ease,background .16s ease}
65
+ .yd-feedback-input{height:40px;padding:0 13px;font-size:14px}
66
+ .yd-feedback-textarea{min-height:118px;padding:13px 14px;font-size:14px;resize:none}
67
+ .yd-feedback-input::placeholder,.yd-feedback-textarea::placeholder{color:var(--yd-feedback-muted-fg)}
68
+ .yd-feedback-input:focus,.yd-feedback-textarea:focus{border-color:var(--yd-feedback-ring);box-shadow:0 0 0 3px color-mix(in srgb,var(--yd-feedback-ring) 18%,transparent);background:var(--yd-feedback-card)}
69
+ .yd-feedback-compose-box{overflow:hidden;border:1px solid var(--yd-feedback-border);border-radius:15px;background:var(--yd-feedback-card)}
70
+ .yd-feedback-compose-box .yd-feedback-textarea{border:0;border-radius:0;box-shadow:none;background:transparent}
71
+ .yd-feedback-attachment-row{display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--yd-feedback-border);padding:9px 12px;color:var(--yd-feedback-muted-fg)}
72
+ .yd-feedback-file-trigger{display:inline-flex;align-items:center;gap:8px;border:0;background:transparent;color:var(--yd-feedback-fg);cursor:pointer;padding:7px 8px;border-radius:10px}
73
+ .yd-feedback-file-trigger:hover{background:var(--yd-feedback-muted)}
74
+ .yd-feedback-file-trigger svg{width:18px;height:18px}
75
+ .yd-feedback-file-input{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;clip-path:inset(50%)}
76
+ .yd-feedback-files{min-width:0;text-align:right;color:var(--yd-feedback-muted-fg);font-size:12px}
77
+ .yd-feedback-select{position:relative;width:142px}
78
+ .yd-feedback-select-button{width:100%;height:40px;display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid var(--yd-feedback-border);border-radius:13px;background:var(--yd-feedback-card);color:var(--yd-feedback-fg);padding:0 12px;cursor:pointer}
79
+ .yd-feedback-select-value{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:700}
80
+ .yd-feedback-select-value svg{width:18px;height:18px;color:var(--yd-feedback-muted-fg)}
81
+ .yd-feedback-select-chevron{width:17px;height:17px;color:var(--yd-feedback-muted-fg)}
82
+ .yd-feedback-select-menu{position:absolute;left:0;top:calc(100% + 7px);z-index:2;display:none;min-width:176px;max-height:220px;overflow:auto;border:1px solid var(--yd-feedback-border);border-radius:13px;background:var(--yd-feedback-card);box-shadow:0 18px 45px rgba(15,23,42,.16);padding:5px}
83
+ .yd-feedback-select[data-open="true"] .yd-feedback-select-menu{display:block}
84
+ .yd-feedback-select-option{width:100%;min-height:38px;display:flex;align-items:center;gap:8px;border:0;border-radius:10px;background:transparent;color:var(--yd-feedback-fg);text-align:left;padding:9px 10px;cursor:pointer}
85
+ .yd-feedback-select-option svg{width:18px;height:18px;flex:0 0 18px;color:var(--yd-feedback-muted-fg)}
86
+ .yd-feedback-select-option span{line-height:1.2;white-space:nowrap}
87
+ .yd-feedback-select-option:hover,.yd-feedback-select-option[data-selected="true"]{background:color-mix(in srgb,var(--yd-feedback-primary) 10%,var(--yd-feedback-card));color:var(--yd-feedback-fg)}
88
+ .yd-feedback-messages{display:flex;min-height:0;flex-direction:column;gap:8px;overflow:auto}
89
+ .yd-feedback-message{border:1px solid var(--yd-feedback-border);border-radius:12px;padding:9px 10px;background:var(--yd-feedback-muted)}
90
+ .yd-feedback-message[data-admin="true"]{background:color-mix(in srgb,var(--yd-feedback-primary) 10%,var(--yd-feedback-card));border-color:color-mix(in srgb,var(--yd-feedback-primary) 24%,var(--yd-feedback-border))}
91
+ .yd-feedback-message-meta{margin-bottom:4px;color:var(--yd-feedback-muted-fg);font-size:11px}
92
+ .yd-feedback-message-content{color:var(--yd-feedback-fg);font-size:12px;line-height:1.45;word-break:break-word}
93
+ .yd-feedback-actions{position:sticky;bottom:0;display:flex;align-items:center;justify-content:flex-end;gap:12px;margin:0 -18px;padding:12px 18px 16px;border-top:1px solid var(--yd-feedback-border);background:var(--yd-feedback-card)}
94
+ .yd-feedback-submit{min-width:104px;height:44px;border:0;border-radius:13px;background:var(--yd-feedback-primary);color:var(--yd-feedback-primary-fg);padding:0 18px;font-size:14px;font-weight:800;cursor:pointer;box-shadow:0 12px 24px color-mix(in srgb,var(--yd-feedback-primary) 18%,transparent);transition:background .16s ease,transform .16s ease}
95
+ .yd-feedback-submit:hover{transform:translateY(-1px)}
96
+ .yd-feedback-submit:disabled{opacity:.55;cursor:not-allowed;transform:none}
97
+ .yd-feedback-secondary{height:38px;border:0;border-radius:12px;background:transparent;color:var(--yd-feedback-fg);padding:0 12px;font-size:14px;font-weight:760;cursor:pointer}
98
+ .yd-feedback-secondary:hover{background:var(--yd-feedback-muted)}
99
+ .yd-feedback-error{min-height:18px;color:#fb7185;font-size:13px}
100
+ @media (max-width:760px){
101
+ .yd-feedback-panel{inset:auto 14px 78px 14px;width:auto;height:min(560px,calc(100vh - 96px));border-radius:16px}
102
+ .yd-feedback-header{padding:16px 16px 14px}
103
+ .yd-feedback-title{font-size:20px}
104
+ .yd-feedback-shell{grid-template-columns:1fr}
105
+ .yd-feedback-sidebar{display:none}
106
+ .yd-feedback-form{padding:14px 14px 0;gap:12px}
107
+ .yd-feedback-row{grid-template-columns:1fr}
108
+ .yd-feedback-actions{justify-content:space-between}
109
+ }
110
+ `;
111
+ document.head.appendChild(style);
112
+ }
113
+ function getBaseUrl(options) {
114
+ return (options.apiBaseUrl || "https://pay.imgto.link").replace(/\/$/, "");
115
+ }
116
+ function safeJson(value) {
117
+ try {
118
+ return JSON.stringify(value);
119
+ } catch {
120
+ return "{}";
121
+ }
122
+ }
123
+ function escapeHtml(value) {
124
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
125
+ }
126
+ function getInitialContact(actor) {
127
+ return actor?.contactEmail || actor?.contactPhone || "";
128
+ }
129
+ function formatCategoryLabel(value, categories = DEFAULT_CATEGORIES) {
130
+ const matched = categories.find((item) => item.value === value);
131
+ return matched?.label || value || DEFAULT_CATEGORIES[0]?.label || "\u53CD\u9988";
132
+ }
133
+ function formatThreadStatus(value) {
134
+ const statusMap = {
135
+ OPEN: "\u5DF2\u63D0\u4EA4",
136
+ WAITING_ADMIN: "\u7B49\u5F85\u56DE\u590D",
137
+ WAITING_USER: "\u5F85\u8865\u5145",
138
+ RESOLVED: "\u5DF2\u5904\u7406",
139
+ CLOSED: "\u5DF2\u5173\u95ED"
140
+ };
141
+ return statusMap[value || ""] || value || "\u5DF2\u63D0\u4EA4";
142
+ }
143
+ function formatSenderType(value) {
144
+ if (value === "END_USER") return "\u7528\u6237";
145
+ if (value === "ADMIN") return "\u7BA1\u7406\u5458";
146
+ return "\u7CFB\u7EDF";
147
+ }
148
+ function formatThreadTime(value) {
149
+ if (!value) return "";
150
+ return new Date(value).toLocaleString();
151
+ }
152
+ async function requestJson(url, init) {
153
+ const response = await fetch(url, {
154
+ ...init,
155
+ headers: {
156
+ Accept: "application/json",
157
+ ...init?.body ? { "Content-Type": "application/json" } : {},
158
+ ...init?.headers || {}
159
+ }
160
+ });
161
+ const payload = await response.json().catch(() => null);
162
+ if (!response.ok || payload?.code !== 200) {
163
+ throw new Error(payload?.message || response.statusText || "Request failed");
164
+ }
165
+ return payload.data;
166
+ }
167
+ function collectMetadata(options) {
168
+ const page = typeof window === "undefined" ? {} : {
169
+ pageUrl: window.location.href,
170
+ pageTitle: document.title,
171
+ userAgent: navigator.userAgent
172
+ };
173
+ return { ...page, ...options.metadata || {} };
174
+ }
175
+ async function compressImage(file) {
176
+ if (typeof document === "undefined" || !file.type.startsWith("image/")) {
177
+ return file;
178
+ }
179
+ const bitmap = await createImageBitmap(file);
180
+ const maxSize = 1600;
181
+ const scale = Math.min(1, maxSize / Math.max(bitmap.width, bitmap.height));
182
+ const canvas = document.createElement("canvas");
183
+ canvas.width = Math.max(1, Math.round(bitmap.width * scale));
184
+ canvas.height = Math.max(1, Math.round(bitmap.height * scale));
185
+ const ctx = canvas.getContext("2d");
186
+ if (!ctx) return file;
187
+ ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
188
+ const blob = await new Promise((resolve) => {
189
+ canvas.toBlob(resolve, "image/webp", 0.82);
190
+ });
191
+ if (!blob) return file;
192
+ return new File([blob], file.name.replace(/\.[^.]+$/, ".webp"), {
193
+ type: "image/webp",
194
+ lastModified: Date.now()
195
+ });
196
+ }
197
+ function createFeedbackWidget(container, options) {
198
+ if (typeof document === "undefined") {
199
+ return { open() {
200
+ }, close() {
201
+ }, destroy() {
202
+ } };
203
+ }
204
+ if (!options.appId) throw new Error("appId is required");
205
+ ensureStyle();
206
+ let threadState = null;
207
+ let pollTimer = null;
208
+ let panelReady = false;
209
+ let pendingFiles = [];
210
+ let renderedMessages = [];
211
+ let removeLoadListener = null;
212
+ const root = document.createElement("div");
213
+ root.className = "yd-feedback-root";
214
+ const launcher = document.createElement("button");
215
+ launcher.type = "button";
216
+ launcher.className = "yd-feedback-launcher";
217
+ launcher.setAttribute("aria-label", options.launcherText || "\u53CD\u9988");
218
+ launcher.innerHTML = FEEDBACK_ICON;
219
+ root.appendChild(launcher);
220
+ container.appendChild(root);
221
+ const panel = document.createElement("section");
222
+ panel.className = "yd-feedback-panel";
223
+ panel.setAttribute("aria-label", options.panelTitle || DEFAULT_PANEL_TITLE);
224
+ panel.innerHTML = `
225
+ <div class="yd-feedback-header">
226
+ <div>
227
+ <h2 class="yd-feedback-title">${escapeHtml(options.panelTitle || DEFAULT_PANEL_TITLE)}</h2>
228
+ <p class="yd-feedback-subtitle">${escapeHtml(options.panelSubtitle || DEFAULT_PANEL_SUBTITLE)}</p>
229
+ </div>
230
+ <button type="button" class="yd-feedback-close" aria-label="\u5173\u95ED">
231
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
232
+ </button>
233
+ </div>
234
+ <div class="yd-feedback-shell">
235
+ <aside class="yd-feedback-sidebar">
236
+ <div class="yd-feedback-sidebar-toolbar">
237
+ <div class="yd-feedback-sidebar-title">${FEEDBACK_ICON}<span>\u53CD\u9988\u8BB0\u5F55</span></div>
238
+ <button type="button" class="yd-feedback-icon-button" data-role="clear" aria-label="\u65B0\u5EFA\u53CD\u9988">
239
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
240
+ </button>
241
+ </div>
242
+ <div class="yd-feedback-thread-list">
243
+ <div class="yd-feedback-thread-card" data-role="thread-card">
244
+ <div class="yd-feedback-thread-badges">
245
+ <span class="yd-feedback-badge" data-role="thread-category">\u95EE\u9898\u53CD\u9988</span>
246
+ <span class="yd-feedback-badge" data-status="true" data-role="thread-status">\u5DF2\u63D0\u4EA4</span>
247
+ </div>
248
+ <p class="yd-feedback-thread-preview" data-role="thread-preview"></p>
249
+ <div class="yd-feedback-thread-meta">
250
+ <span data-role="thread-time"></span>
251
+ <span>\u6211\u7684\u53CD\u9988</span>
252
+ </div>
253
+ </div>
254
+ <div class="yd-feedback-empty-thread" data-role="thread-empty">\u63D0\u4EA4\u53CD\u9988\u540E\uFF0C\u8BB0\u5F55\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\u3002</div>
255
+ </div>
256
+ </aside>
257
+ <div class="yd-feedback-main">
258
+ <div class="yd-feedback-form">
259
+ <div class="yd-feedback-row">
260
+ <div class="yd-feedback-field">
261
+ <label class="yd-feedback-label">\u53CD\u9988\u7C7B\u578B</label>
262
+ <div class="yd-feedback-select" data-role="category-select">
263
+ <button type="button" class="yd-feedback-select-button" data-role="category-trigger">
264
+ <span class="yd-feedback-select-value">${BUG_ICON}<span data-role="category-value">\u95EE\u9898\u53CD\u9988</span></span>
265
+ <span class="yd-feedback-select-chevron">${CHEVRON_ICON}</span>
266
+ </button>
267
+ <div class="yd-feedback-select-menu" data-role="category-menu"></div>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ <div class="yd-feedback-field">
272
+ <label class="yd-feedback-label">${NOTE_ICON}<span>\u53CD\u9988\u5185\u5BB9</span></label>
273
+ <div class="yd-feedback-compose-box">
274
+ <textarea class="yd-feedback-textarea" data-role="content" placeholder="${escapeHtml(options.placeholder || "\u8BF7\u63CF\u8FF0\u95EE\u9898\u73B0\u8C61\u3001\u64CD\u4F5C\u6B65\u9AA4\u6216\u5EFA\u8BAE\u5185\u5BB9\u3002")}"></textarea>
275
+ <div class="yd-feedback-attachment-row">
276
+ <button type="button" class="yd-feedback-file-trigger" data-role="files-trigger">${IMAGE_ICON}<span>\u4E0A\u4F20\u56FE\u7247</span></button>
277
+ <input class="yd-feedback-file-input" type="file" accept="image/png,image/jpeg,image/webp" multiple data-role="files" />
278
+ <div class="yd-feedback-files" data-role="files-label">${DEFAULT_ATTACHMENTS_LABEL}</div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ <div class="yd-feedback-field">
283
+ <label class="yd-feedback-label">${MAIL_ICON}<span>\u8054\u7CFB\u65B9\u5F0F\uFF08\u9009\u586B\uFF09</span></label>
284
+ <input class="yd-feedback-input" data-role="contact" value="${escapeHtml(getInitialContact(options.actor))}" placeholder="${escapeHtml(options.contactPlaceholder || "\u90AE\u7BB1\u6216\u7535\u8BDD\u53F7\u7801\uFF0C\u4EC5\u7528\u4E8E\u53CD\u9988\u8DDF\u8FDB")}" />
285
+ </div>
286
+ <div class="yd-feedback-messages" data-role="messages"></div>
287
+ <div class="yd-feedback-error" data-role="error"></div>
288
+ <div class="yd-feedback-actions">
289
+ <button type="button" class="yd-feedback-secondary" data-role="clear-secondary">\u5173\u95ED</button>
290
+ <button type="button" class="yd-feedback-submit" data-role="submit">\u63D0\u4EA4\u53CD\u9988</button>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </div>
295
+ `;
296
+ function initPanel() {
297
+ if (panelReady) return;
298
+ panelReady = true;
299
+ root.appendChild(panel);
300
+ const categorySelect = panel.querySelector(
301
+ '[data-role="category-select"]'
302
+ );
303
+ const categoryMenu = panel.querySelector(
304
+ '[data-role="category-menu"]'
305
+ );
306
+ const categoryValue = panel.querySelector(
307
+ '[data-role="category-value"]'
308
+ );
309
+ if (categoryMenu && categoryValue) {
310
+ const categories = options.categories || DEFAULT_CATEGORIES;
311
+ categoryValue.textContent = categories[0]?.label || DEFAULT_CATEGORIES[0]?.label || "\u53CD\u9988";
312
+ for (const item of categories) {
313
+ const option = document.createElement("button");
314
+ option.type = "button";
315
+ option.className = "yd-feedback-select-option";
316
+ option.dataset.value = item.value;
317
+ option.dataset.selected = String(item.value === categories[0]?.value);
318
+ option.innerHTML = `${BUG_ICON}<span>${escapeHtml(item.label)}</span>`;
319
+ option.addEventListener("click", () => {
320
+ for (const node of categoryMenu.querySelectorAll(
321
+ ".yd-feedback-select-option"
322
+ )) {
323
+ node.dataset.selected = String(node === option);
324
+ }
325
+ categoryValue.textContent = item.label;
326
+ if (categorySelect) {
327
+ categorySelect.dataset.value = item.value;
328
+ categorySelect.dataset.open = "false";
329
+ }
330
+ });
331
+ categoryMenu.appendChild(option);
332
+ }
333
+ categorySelect?.setAttribute("data-value", categories[0]?.value || "BUG");
334
+ }
335
+ panel.querySelector('[data-role="category-trigger"]')?.addEventListener("click", () => {
336
+ if (!categorySelect) return;
337
+ categorySelect.dataset.open = categorySelect.dataset.open === "true" ? "false" : "true";
338
+ });
339
+ panel.querySelector(".yd-feedback-close")?.addEventListener("click", close);
340
+ panel.querySelector('[data-role="clear"]')?.addEventListener("click", resetThread);
341
+ panel.querySelector('[data-role="clear-secondary"]')?.addEventListener("click", close);
342
+ panel.querySelector('[data-role="files-trigger"]')?.addEventListener("click", () => {
343
+ panel.querySelector('[data-role="files"]')?.click();
344
+ });
345
+ panel.querySelector('[data-role="files"]')?.addEventListener("change", (event) => {
346
+ setPendingFiles(event.target.files);
347
+ });
348
+ const composeBox = panel.querySelector(
349
+ ".yd-feedback-compose-box"
350
+ );
351
+ composeBox?.addEventListener("dragover", (event) => {
352
+ event.preventDefault();
353
+ });
354
+ composeBox?.addEventListener("drop", (event) => {
355
+ event.preventDefault();
356
+ setPendingFiles(event.dataTransfer?.files || null);
357
+ });
358
+ composeBox?.addEventListener("paste", (event) => {
359
+ const files = Array.from(event.clipboardData?.files || []).filter(
360
+ (file) => file.type.startsWith("image/")
361
+ );
362
+ if (files.length > 0) {
363
+ setPendingFiles(files);
364
+ }
365
+ });
366
+ panel.querySelector('[data-role="submit"]')?.addEventListener("click", () => void submit());
367
+ }
368
+ function setPendingFiles(files) {
369
+ pendingFiles = Array.from(files || []).filter((file) => file.type.startsWith("image/")).slice(0, MAX_IMAGES_PER_MESSAGE);
370
+ const label = panel.querySelector('[data-role="files-label"]');
371
+ if (label) {
372
+ label.textContent = pendingFiles.length > 0 ? `\u5DF2\u9009\u62E9 ${pendingFiles.length} \u5F20\u56FE\u7247` : DEFAULT_ATTACHMENTS_LABEL;
373
+ }
374
+ }
375
+ function resetThread() {
376
+ threadState = null;
377
+ pendingFiles = [];
378
+ renderedMessages = [];
379
+ renderMessages([]);
380
+ updateThreadCard();
381
+ setError("");
382
+ const textarea = panel.querySelector(
383
+ '[data-role="content"]'
384
+ );
385
+ if (textarea) textarea.value = "";
386
+ const fileInput = panel.querySelector(
387
+ '[data-role="files"]'
388
+ );
389
+ if (fileInput) fileInput.value = "";
390
+ const filesLabel = panel.querySelector(
391
+ '[data-role="files-label"]'
392
+ );
393
+ if (filesLabel) filesLabel.textContent = DEFAULT_ATTACHMENTS_LABEL;
394
+ }
395
+ function updateThreadCard() {
396
+ const card = panel.querySelector('[data-role="thread-card"]');
397
+ const empty = panel.querySelector('[data-role="thread-empty"]');
398
+ if (!card || !empty) return;
399
+ if (!threadState) {
400
+ card.dataset.visible = "false";
401
+ empty.style.display = "block";
402
+ return;
403
+ }
404
+ card.dataset.visible = "true";
405
+ empty.style.display = "none";
406
+ const category = card.querySelector(
407
+ '[data-role="thread-category"]'
408
+ );
409
+ const status = card.querySelector(
410
+ '[data-role="thread-status"]'
411
+ );
412
+ const preview = card.querySelector(
413
+ '[data-role="thread-preview"]'
414
+ );
415
+ const time = card.querySelector('[data-role="thread-time"]');
416
+ if (category) {
417
+ category.textContent = formatCategoryLabel(
418
+ threadState.category || DEFAULT_CATEGORIES[0]?.value,
419
+ options.categories || DEFAULT_CATEGORIES
420
+ );
421
+ }
422
+ if (status) status.textContent = formatThreadStatus(threadState.status);
423
+ if (preview) preview.textContent = threadState.contentPreview || "";
424
+ if (time) time.textContent = formatThreadTime(threadState.createdAt);
425
+ }
426
+ function setError(message) {
427
+ const node = panel.querySelector('[data-role="error"]');
428
+ if (node) node.textContent = message;
429
+ if (message) options.onError?.(new Error(message));
430
+ }
431
+ function setSubmitting(value) {
432
+ const button = panel.querySelector(
433
+ '[data-role="submit"]'
434
+ );
435
+ if (button) button.disabled = value;
436
+ }
437
+ function renderMessages(messages) {
438
+ const node = panel.querySelector('[data-role="messages"]');
439
+ if (!node) return;
440
+ renderedMessages = messages;
441
+ node.innerHTML = "";
442
+ for (const message of messages) {
443
+ const item = document.createElement("div");
444
+ item.className = "yd-feedback-message";
445
+ item.dataset.admin = String(message.senderType !== "END_USER");
446
+ item.innerHTML = `<div class="yd-feedback-message-meta">${formatSenderType(message.senderType)} \xB7 ${new Date(message.createdAt).toLocaleString()}</div><div class="yd-feedback-message-content"></div>`;
447
+ const content = item.querySelector(".yd-feedback-message-content");
448
+ if (content) {
449
+ content.textContent = message.senderType === "END_USER" ? message.content : message.translatedContent || message.content;
450
+ }
451
+ node.appendChild(item);
452
+ }
453
+ }
454
+ function appendMessages(messages) {
455
+ const byId = /* @__PURE__ */ new Map();
456
+ for (const message of renderedMessages) byId.set(message.id, message);
457
+ for (const message of messages) byId.set(message.id, message);
458
+ renderMessages(Array.from(byId.values()));
459
+ }
460
+ async function uploadImages(files) {
461
+ const selected = files.slice(0, MAX_IMAGES_PER_MESSAGE);
462
+ const ids = [];
463
+ for (const file of selected) {
464
+ const compressed = await compressImage(file);
465
+ const body = new FormData();
466
+ body.append("file", compressed);
467
+ if (threadState) body.append("threadId", threadState.threadId);
468
+ const headers = {};
469
+ if (threadState) headers["x-feedback-token"] = threadState.clientToken;
470
+ const response = await fetch(
471
+ `${getBaseUrl(options)}/api/v1/feedback/${encodeURIComponent(options.appId)}/resources/images`,
472
+ { method: "POST", body, headers }
473
+ );
474
+ const payload = await response.json();
475
+ if (!response.ok || payload?.code !== 200) {
476
+ throw new Error(payload?.message || "Image upload failed");
477
+ }
478
+ ids.push(payload.data.resource.id);
479
+ }
480
+ return ids;
481
+ }
482
+ async function submit() {
483
+ const content = panel.querySelector('[data-role="content"]')?.value.trim();
484
+ if (!content) {
485
+ setError("\u8BF7\u8F93\u5165\u53CD\u9988\u5185\u5BB9\u3002");
486
+ return;
487
+ }
488
+ setSubmitting(true);
489
+ setError("");
490
+ try {
491
+ const category = panel.querySelector('[data-role="category-select"]')?.dataset.value || "OTHER";
492
+ const contact = panel.querySelector('[data-role="contact"]')?.value || "";
493
+ const fileInput = panel.querySelector(
494
+ '[data-role="files"]'
495
+ );
496
+ const attachments = await uploadImages(pendingFiles);
497
+ if (!threadState) {
498
+ const result = await requestJson(
499
+ `${getBaseUrl(options)}/api/v1/feedback/${encodeURIComponent(options.appId)}/threads`,
500
+ {
501
+ method: "POST",
502
+ body: safeJson({
503
+ category,
504
+ content,
505
+ attachments,
506
+ actor: {
507
+ ...options.actor || {},
508
+ contactEmail: contact.includes("@") ? contact : options.actor?.contactEmail,
509
+ contactPhone: contact && !contact.includes("@") ? contact : options.actor?.contactPhone
510
+ },
511
+ metadata: collectMetadata(options)
512
+ })
513
+ }
514
+ );
515
+ threadState = {
516
+ threadId: result.thread.id,
517
+ clientToken: result.clientToken,
518
+ lastMessageId: result.message?.id,
519
+ status: result.thread.status,
520
+ category,
521
+ contentPreview: content,
522
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
523
+ };
524
+ updateThreadCard();
525
+ options.onThreadCreated?.(result.thread.id);
526
+ options.onMessageSent?.(result.message.id);
527
+ } else {
528
+ const result = await requestJson(
529
+ `${getBaseUrl(options)}/api/v1/feedback/${encodeURIComponent(options.appId)}/threads/${encodeURIComponent(threadState.threadId)}/messages`,
530
+ {
531
+ method: "POST",
532
+ headers: { "x-feedback-token": threadState.clientToken },
533
+ body: safeJson({
534
+ content,
535
+ attachments,
536
+ metadata: collectMetadata(options)
537
+ })
538
+ }
539
+ );
540
+ threadState.lastMessageId = result.message.id;
541
+ threadState.contentPreview = content;
542
+ options.onMessageSent?.(result.message.id);
543
+ }
544
+ const textarea = panel.querySelector(
545
+ '[data-role="content"]'
546
+ );
547
+ if (textarea) textarea.value = "";
548
+ if (fileInput) fileInput.value = "";
549
+ pendingFiles = [];
550
+ const filesLabel = panel.querySelector(
551
+ '[data-role="files-label"]'
552
+ );
553
+ if (filesLabel) filesLabel.textContent = DEFAULT_ATTACHMENTS_LABEL;
554
+ await pollOnce();
555
+ startPolling();
556
+ } catch (error) {
557
+ setError(error instanceof Error ? error.message : "\u63D0\u4EA4\u5931\u8D25");
558
+ } finally {
559
+ setSubmitting(false);
560
+ }
561
+ }
562
+ async function pollOnce() {
563
+ if (!threadState) return;
564
+ const currentThread = threadState;
565
+ const params = new URLSearchParams();
566
+ if (currentThread.lastMessageId) {
567
+ params.set("afterMessageId", currentThread.lastMessageId);
568
+ }
569
+ params.set("feedbackToken", currentThread.clientToken);
570
+ const result = await requestJson(
571
+ `${getBaseUrl(options)}/api/v1/feedback/${encodeURIComponent(options.appId)}/threads/${encodeURIComponent(currentThread.threadId)}/messages?${params.toString()}`
572
+ );
573
+ currentThread.status = result.threadStatus;
574
+ updateThreadCard();
575
+ if (result.messages.length > 0) {
576
+ const latestMessage = result.messages.at(-1);
577
+ if (latestMessage) currentThread.lastMessageId = latestMessage.id;
578
+ appendMessages(result.messages);
579
+ }
580
+ if (result.threadStatus !== "WAITING_ADMIN") stopPolling();
581
+ else schedulePoll(result.nextPollAfterMs);
582
+ }
583
+ function schedulePoll(delay) {
584
+ stopPolling();
585
+ if (!delay || delay < 1 || document.hidden || !threadState) return;
586
+ pollTimer = window.setTimeout(() => void pollOnce(), delay);
587
+ }
588
+ function startPolling() {
589
+ if (!threadState || threadState.status !== "WAITING_ADMIN") return;
590
+ schedulePoll(1e4);
591
+ }
592
+ function stopPolling() {
593
+ if (pollTimer) window.clearTimeout(pollTimer);
594
+ pollTimer = null;
595
+ }
596
+ function open() {
597
+ initPanel();
598
+ panel.dataset.open = "true";
599
+ window.setTimeout(() => {
600
+ panel.querySelector('[data-role="content"]')?.focus();
601
+ }, 0);
602
+ }
603
+ function close() {
604
+ panel.dataset.open = "false";
605
+ stopPolling();
606
+ }
607
+ launcher.addEventListener("click", open);
608
+ const handleVisibilityChange = () => {
609
+ if (document.hidden) stopPolling();
610
+ else startPolling();
611
+ };
612
+ const handleDocumentClick = (event) => {
613
+ const categorySelect = panel.querySelector(
614
+ '[data-role="category-select"]'
615
+ );
616
+ if (categorySelect && event.target instanceof Node && !categorySelect.contains(event.target)) {
617
+ categorySelect.dataset.open = "false";
618
+ }
619
+ };
620
+ document.addEventListener("visibilitychange", handleVisibilityChange);
621
+ document.addEventListener("click", handleDocumentClick);
622
+ if (typeof window !== "undefined") {
623
+ const preload = () => {
624
+ if ("requestIdleCallback" in window) {
625
+ ;
626
+ window.requestIdleCallback(() => initPanel());
627
+ } else {
628
+ globalThis.setTimeout(() => initPanel(), 1200);
629
+ }
630
+ };
631
+ if (document.readyState === "complete") preload();
632
+ else {
633
+ window.addEventListener("load", preload, { once: true });
634
+ removeLoadListener = () => window.removeEventListener("load", preload);
635
+ }
636
+ }
637
+ return {
638
+ open,
639
+ close,
640
+ destroy() {
641
+ stopPolling();
642
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
643
+ document.removeEventListener("click", handleDocumentClick);
644
+ removeLoadListener?.();
645
+ root.remove();
646
+ }
647
+ };
648
+ }
649
+ function mountFeedbackWidget(container, options) {
650
+ return createFeedbackWidget(container, options);
651
+ }
652
+ export {
653
+ createFeedbackWidget,
654
+ mountFeedbackWidget
655
+ };
656
+ //# sourceMappingURL=feedback.js.map