Files
2026-09-29 15:30:33 +07:00

310 lines
9.7 KiB
JavaScript

(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 = '<img class="icon" src="/static/images/placeholder.png?v=2" data-src="/static/images/close.svg" alt="" loading="lazy" decoding="async">';
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);
});
});
})();