(function () { var form = document.querySelector("[data-feedback-form]"); if (!form) return; var types = form.querySelectorAll(".feedback-type"); var content = form.querySelector("[data-feedback-content]"); var count = form.querySelector("[data-feedback-count]"); var uploads = form.querySelector("[data-feedback-uploads]"); var fileInput = form.querySelector("[data-feedback-file]"); var submitBtn = form.querySelector(".feedback-form__submit"); var files = []; var maxFiles = 3; var submitting = false; var ALLOWED_MIME = { "image/jpeg": true, "image/jpg": true, "image/pjpeg": true, "image/png": true, "image/gif": true, }; var ALLOWED_EXT = { jpg: true, jpeg: true, png: true, gif: true, }; var TYPE_MAP = { play: 2, // 播放问题 account: 3, // 账号问题 report: 4, // 内容举报 → 其他 suggest: 1, // 功能建议 other: 4, // 其他 }; function toast(message, type) { if (typeof window.showToast === "function") { window.showToast(message, type); return; } window.alert(message); } Array.prototype.forEach.call(types, function (type) { var input = type.querySelector('input[type="radio"]'); if (!input) return; input.addEventListener("change", function () { Array.prototype.forEach.call(types, function (item) { item.classList.toggle("is-active", item.querySelector("input").checked); }); }); }); if (content && count) { content.addEventListener("input", function () { count.textContent = String(Array.from(content.value || "").length); }); } function isAllowedImage(file) { if (!file) return false; var mime = String(file.type || "").toLowerCase(); if (mime && ALLOWED_MIME[mime]) return true; var name = String(file.name || ""); var dot = name.lastIndexOf("."); if (dot < 0) return false; var ext = name.slice(dot + 1).toLowerCase(); return !!ALLOWED_EXT[ext]; } function renderUploads() { if (!uploads || !fileInput) return; uploads.innerHTML = ""; files.forEach(function (file, index) { var item = document.createElement("div"); item.className = "feedback-upload feedback-upload--preview"; if (file.uploading) item.classList.add("is-uploading"); if (file.error) item.classList.add("is-error"); var img = document.createElement("img"); img.src = "/static/images/placeholder.png?v=2"; img.setAttribute("data-src", file.previewUrl); img.alt = file.name || "截图"; img.loading = "lazy"; img.decoding = "async"; var remove = document.createElement("button"); remove.type = "button"; remove.className = "feedback-upload__remove"; remove.setAttribute("aria-label", "删除截图"); remove.innerHTML = ''; remove.addEventListener("click", function () { if (file.previewUrl) URL.revokeObjectURL(file.previewUrl); files.splice(index, 1); renderUploads(); }); item.appendChild(img); item.appendChild(remove); uploads.appendChild(item); }); if (files.length < maxFiles) { var label = document.createElement("label"); label.className = "feedback-upload feedback-upload--add"; label.appendChild(fileInput); fileInput.hidden = true; var icon = document.createElement("img"); icon.className = "icon"; icon.src = "/static/images/placeholder.png?v=2"; icon.setAttribute("data-src", "/static/images/plus-dim.svg"); icon.alt = ""; icon.loading = "lazy"; icon.decoding = "async"; label.appendChild(icon); var text = document.createElement("span"); text.textContent = "上传截图"; label.appendChild(text); uploads.appendChild(label); } else { uploads.appendChild(fileInput); fileInput.hidden = true; } if (typeof window.initLazyImages === "function") { window.initLazyImages(uploads); } } function extractUploadURL(data) { if (!data) return ""; if (typeof data === "string") return data; return String(data.sourceNameFs || data.url || data.path || data.id || "").trim(); } function uploadOne(fileItem) { if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { return Promise.reject(new Error("上传服务未就绪")); } if (fileItem.remoteURL) return Promise.resolve(fileItem.remoteURL); fileItem.uploading = true; renderUploads(); var fd = new FormData(); fd.append("file", fileItem.file, fileItem.name || "image.jpg"); return window.HttpRequest.post("/api/upload/image", fd) .then(function (res) { if (!res || Number(res.code) !== 200 || !res.data) { throw new Error((res && (res.tip || res.msg)) || "上传失败"); } var url = extractUploadURL(res.data); if (!url) throw new Error("上传成功但未返回图片地址"); fileItem.remoteURL = url; fileItem.uploading = false; fileItem.error = false; renderUploads(); return url; }) .catch(function (err) { fileItem.uploading = false; fileItem.error = true; renderUploads(); throw err; }); } function uploadAll() { var chain = Promise.resolve([]); files.forEach(function (item) { chain = chain.then(function (urls) { return uploadOne(item).then(function (url) { urls.push(url); return urls; }); }); }); return chain; } function selectedType() { var checked = form.querySelector('input[name="type"]:checked'); var key = checked ? String(checked.value || "") : "play"; return TYPE_MAP[key] || 2; } function apiTip(payload, fallback) { if (!payload) return fallback; if (payload.tip || payload.msg) return String(payload.tip || payload.msg); if (payload.data && (payload.data.tip || payload.data.msg)) { return String(payload.data.tip || payload.data.msg); } if (payload.message) return String(payload.message); return fallback; } function setBusy(on) { submitting = !!on; if (submitBtn) { submitBtn.disabled = !!on; submitBtn.classList.toggle("is-loading", !!on); if (on) { if (!submitBtn.dataset.labelHtml) { submitBtn.dataset.labelHtml = submitBtn.innerHTML; } submitBtn.textContent = "提交中…"; } else if (submitBtn.dataset.labelHtml) { submitBtn.innerHTML = submitBtn.dataset.labelHtml; } } if (content) content.disabled = !!on; if (fileInput) fileInput.disabled = !!on; Array.prototype.forEach.call(form.querySelectorAll("input, textarea, button"), function (el) { if (el === submitBtn) return; if (el === fileInput) return; if (el === content) return; if (el.type === "file") return; el.disabled = !!on; }); } if (fileInput) { fileInput.addEventListener("change", function () { var selected = Array.prototype.slice.call(fileInput.files || []); var rejectedFormat = false; var rejectedFull = false; selected.forEach(function (file) { if (files.length >= maxFiles) { rejectedFull = true; return; } if (!isAllowedImage(file)) { rejectedFormat = true; return; } files.push({ name: file.name, file: file, previewUrl: URL.createObjectURL(file), remoteURL: "", uploading: false, error: false, }); }); fileInput.value = ""; renderUploads(); if (rejectedFormat) toast("仅支持 jpg / png / gif 格式", "error"); else if (rejectedFull) toast("最多上传 3 张图片", "error"); }); } renderUploads(); form.addEventListener("submit", function (event) { event.preventDefault(); if (submitting) return; if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { toast("反馈服务未就绪", "error"); return; } var text = String((content && content.value) || "").trim(); if (!text) { toast("请填写问题描述", "error"); return; } if (Array.from(text).length > 500) { toast("问题描述最多 500 字", "error"); return; } var contactInput = form.querySelector('input[name="contact"]'); var contact = contactInput ? String(contactInput.value || "").trim() : ""; var type = selectedType(); setBusy(true); uploadAll() .then(function (screenshots) { var payload = { type: type, content: text, contact: contact, screenshots: screenshots || [], }; return window.HttpRequest.post("/api/feedback/add", payload); }) .then(function (res) { if (!res || Number(res.code) !== 200) { var failTip = apiTip(res, "提交失败"); var failErr = new Error(failTip); failErr.data = res; throw failErr; } toast("反馈提交成功", "success"); form.reset(); files.forEach(function (f) { if (f.previewUrl) URL.revokeObjectURL(f.previewUrl); }); files = []; Array.prototype.forEach.call(types, function (item) { var input = item.querySelector("input"); item.classList.toggle("is-active", !!(input && input.checked)); }); if (count) count.textContent = "0"; renderUploads(); }) .catch(function (err) { toast(apiTip(err, "提交失败,请稍后重试"), "error"); }) .finally(function () { setBusy(false); }); }); })();