+ ` : ""}
+
+
+ ${isPublisher && task.status === "open" && (task.applicants || []).length > 0 ? `
+
{
+ window.dispatchEvent(new CustomEvent("comfy-route-edit-task", {
+ detail: { taskData: task, currentUser }
+ }));
+ });
+
+ // 绑定删除按钮(仅open状态可删除)
+ contentEl.querySelector("#btn-delete-task")?.addEventListener("click", () => {
+ showDeleteTaskConfirm(task, contentEl);
+ });
+
+ // 绑定申请者列表的选择按钮
+ contentEl.querySelectorAll(".btn-assign").forEach(btn => {
+ btn.onclick = () => {
+ const assignee = btn.dataset.account;
+ showAssignConfirmDialog(task, assignee, contentEl, currentUser);
+ };
+ });
+
+ // 绑定图片沙盒交互(缩放、拖动、轮播等)
+ setupImageSandboxEvents(contentEl);
+
+ // 👍 绑定互动事件(点赞/收藏/打赏)
+ bindTaskInteractionEvents(contentEl, task, currentUser);
+
+ // 💬 加载评论
+ loadTaskComments(contentEl, task.id, currentUser);
+}
+
+/**
+ * 🔧 通用确认对话框工厂函数
+ */
+function _showConfirmDialog({ title, icon, message, description, confirmText, confirmStyle, loadingText, errorText, width, onConfirm }) {
+ const content = document.createElement("div");
+ content.style.color = "#ccc";
+ content.innerHTML = `
+
+
${icon}
+
${message}
+
${description}
+
+
+
+
+
+ `;
+
+ content.querySelector("#dialog-cancel").onclick = () => globalModal.closeTopModal();
+
+ content.querySelector("#dialog-confirm").onclick = async () => {
+ const confirmBtn = content.querySelector("#dialog-confirm");
+ confirmBtn.disabled = true;
+ confirmBtn.textContent = loadingText || `⏳ ${t('common.confirming')}...`;
+
+ try {
+ await onConfirm();
+ } catch (err) {
+ showToast(err.message || errorText, "error");
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = confirmText;
+ }
+ };
+
+ globalModal.openModal(title, content, { width: width || "350px" });
+}
+
+/**
+ * 👤 显示接受任务申请确认对话框
+ */
+function showAssignConfirmDialog(task, assignee, contentEl, currentUser) {
+ _showConfirmDialog({
+ title: t('task.choose_assignee'),
+ icon: '👤',
+ message: t('task.confirm_assign', { assignee }),
+ description: t('task.confirm_deposit', { amount: task.deposit_amount }),
+ confirmText: t('common.confirm'),
+ confirmStyle: 'background: #4CAF50',
+ errorText: t('task.assign_failed'),
+ onConfirm: async () => {
+ await api.assignTask(task.id, assignee);
+ showToast(t('task.assign_success'), "success");
+ globalModal.closeTopModal();
+ loadTaskDetail(contentEl.closest("div"), task.id, currentUser);
+ }
+ });
+}
+
+/**
+ * 🙋 显示申请接单对话框
+ */
+function showApplyDialog(task, currentUser) {
+ const content = document.createElement("div");
+ content.style.color = "#ccc";
+ content.innerHTML = `
+
+
${t('task.apply_message_prompt')}
+
+
+
+
+
+
+ `;
+
+ content.querySelector("#apply-cancel").onclick = () => globalModal.closeTopModal();
+
+ content.querySelector("#apply-confirm").onclick = async () => {
+ const message = content.querySelector("#apply-message").value;
+ const confirmBtn = content.querySelector("#apply-confirm");
+ confirmBtn.disabled = true;
+ confirmBtn.textContent = `⏳ ${t('common.submitting')}...`;
+
+ try {
+ await api.applyTask(task.id, message);
+ showToast(t('task.apply_success'), "success");
+ globalModal.closeTopModal();
+ // 重新加载任务详情
+ const container = document.querySelector('#task-content')?.parentElement;
+ if (container && currentUser) {
+ loadTaskDetail(container, task.id, currentUser);
+ }
+ } catch (err) {
+ showToast(err.message || t('task.apply_failed'), "error");
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = t('common.confirm_submit');
+ }
+ };
+
+ globalModal.openModal(t('task.apply_task'), content, { width: "400px" });
+}
+
+/**
+ * ↩️ 显示撤回申请确认对话框
+ */
+function showCancelApplyConfirmDialog(task, currentUser) {
+ _showConfirmDialog({
+ title: t('task.cancel_apply'),
+ icon: '↩️',
+ message: t('task.confirm_cancel_apply'),
+ description: t('task.cancel_apply_warning') || '',
+ confirmText: t('common.confirm'),
+ confirmStyle: 'background: #FF9800',
+ errorText: t('task.cancel_apply_failed'),
+ onConfirm: async () => {
+ await api.cancelApplyTask(task.id);
+ showToast(t('task.apply_cancelled'), "success");
+ globalModal.closeTopModal();
+ const container = document.querySelector('#task-content')?.parentElement;
+ if (container && currentUser) {
+ loadTaskDetail(container, task.id, currentUser);
+ }
+ }
+ });
+}
+
+/**
+ * ❌ 显示取消任务确认对话框
+ */
+function showCancelTaskConfirmDialog(task) {
+ _showConfirmDialog({
+ title: t('task.cancel_task'),
+ icon: '⚠️',
+ message: t('task.confirm_cancel_task'),
+ description: t('task.cancel_task_warning') || '',
+ confirmText: t('task.cancel_task'),
+ confirmStyle: 'background: #F44336',
+ loadingText: `⏳ ${t('common.cancelling')}...`,
+ errorText: t('task.cancel_task_failed'),
+ onConfirm: async () => {
+ await api.cancelTask(task.id);
+ showToast(t('task.task_cancelled'), "success");
+ globalModal.closeTopModal();
+ window.dispatchEvent(new CustomEvent("comfy-route-back"));
+ }
+ });
+}
+
+/**
+ * ✅ 显示验收通过确认对话框
+ */
+function showAcceptConfirmDialog(task, currentUser) {
+ _showConfirmDialog({
+ title: t('task.accept_work'),
+ icon: '✅',
+ message: t('task.confirm_accept'),
+ description: t('task.confirm_pay_remaining', { amount: task.total_price - task.deposit_amount }),
+ confirmText: t('task.accept_work'),
+ confirmStyle: 'background: #4CAF50',
+ errorText: t('common.operation_failed'),
+ onConfirm: async () => {
+ await api.acceptTask(task.id, true);
+ showToast(t('task.accept_success'), "success");
+ globalModal.closeTopModal();
+ const container = document.querySelector('#task-content')?.parentElement;
+ if (container && currentUser) {
+ loadTaskDetail(container, task.id, currentUser);
+ }
+ }
+ });
+}
+
+/**
+ * ❌ 显示验收拒绝对话框
+ */
+function showRejectDialog(task, currentUser) {
+ const content = document.createElement("div");
+ content.style.color = "#ccc";
+ content.innerHTML = `
+
+
${t('task.reject_reason_prompt')}
+
+
+
+
+
+
+ `;
+
+ content.querySelector("#reject-cancel").onclick = () => globalModal.closeTopModal();
+
+ content.querySelector("#reject-confirm").onclick = async () => {
+ const feedback = content.querySelector("#reject-feedback").value.trim();
+ if (!feedback) {
+ showToast(t('task.reject_reason_required') || '请输入拒绝原因', "warning");
+ return;
+ }
+
+ const confirmBtn = content.querySelector("#reject-confirm");
+ confirmBtn.disabled = true;
+ confirmBtn.textContent = `⏳ ${t('common.submitting')}...`;
+
+ try {
+ await api.acceptTask(task.id, false, feedback);
+ showToast(t('task.work_returned'), "success");
+ globalModal.closeTopModal();
+ // 重新加载任务详情
+ const container = document.querySelector('#task-content')?.parentElement;
+ if (container && currentUser) {
+ loadTaskDetail(container, task.id, currentUser);
+ }
+ } catch (err) {
+ showToast(err.message || t('common.operation_failed'), "error");
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = t('task.reject_work');
+ }
+ };
+
+ globalModal.openModal(t('task.reject_work'), content, { width: "400px" });
+}
+
+/**
+ * 🗑️ 显示删除评论确认对话框
+ */
+function showDeleteCommentConfirmDialog(taskId, comment, container, currentUser) {
+ const content = document.createElement("div");
+ content.style.color = "#ccc";
+ content.innerHTML = `
+
+
⚠️
+
${t('post.delete_comment_confirm') || '确定删除这条评论吗?'}
+
${t('post.delete_comment_warning') || '删除后无法恢复'}
+
+
+
+
+
+ `;
+
+ content.querySelector("#delete-comment-cancel").onclick = () => globalModal.closeTopModal();
+
+ content.querySelector("#delete-comment-confirm").onclick = async () => {
+ const confirmBtn = content.querySelector("#delete-comment-confirm");
+ confirmBtn.disabled = true;
+ confirmBtn.textContent = `⏳ ${t('common.deleting')}...`;
+
+ try {
+ await api.deleteComment(taskId, comment.id);
+ showToast(t('post.delete_comment_success') || '评论已删除', "success");
+ globalModal.closeTopModal();
+ loadTaskComments(container, taskId, currentUser);
+ } catch (err) {
+ showToast((t('post.delete_comment_failed') || '删除失败') + ": " + err.message, "error");
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = t('common.delete');
+ }
+ };
+
+ globalModal.openModal(t('common.delete'), content, { width: "350px" });
+}
+
+/**
+ * 🎮 渲染操作按钮
+ */
+function renderActionButtons(container, task, currentUser, isPublisher, isAssignee, hasApplied) {
+ let buttons = [];
+
+ if (!currentUser) {
+ buttons.push(`
${t('common.login_required')}
`);
+ } else if (task.status === "open") {
+ if (isPublisher) {
+ buttons.push(`
+
+ `);
+ } else if (hasApplied) {
+ buttons.push(`
+
+ `);
+ } else {
+ buttons.push(`
+
+ `);
+ }
+ } else if (task.status === "in_progress") {
+ if (isAssignee) {
+ buttons.push(`
+
+ `);
+ }
+ } else if (task.status === "submitted") {
+ if (isPublisher) {
+ buttons.push(`
+
+
+ `);
+ }
+ }
+
+ // 申诉按钮(进行中或待验收时,双方都可以申诉)
+ if ((task.status === "in_progress" || task.status === "submitted") && (isPublisher || isAssignee)) {
+ buttons.push(`
+
+ `);
+ }
+
+ // 查看申诉详情按钮(争议中状态)
+ if (task.status === "disputed" && task.dispute_id && (isPublisher || isAssignee)) {
+ buttons.push(`
+
+ `);
+ }
+
+ container.innerHTML = buttons.join("");
+
+ // 绑定事件
+ bindActionEvents(container, task, currentUser);
+}
+
+/**
+ * 🔌 绑定操作按钮事件
+ */
+function bindActionEvents(container, task, currentUser) {
+ // 申请接单
+ container.querySelector("#btn-apply")?.addEventListener("click", () => {
+ showApplyDialog(task, currentUser);
+ });
+
+ // 撤回申请
+ container.querySelector("#btn-cancel-apply")?.addEventListener("click", () => {
+ showCancelApplyConfirmDialog(task, currentUser);
+ });
+
+ // 取消任务
+ container.querySelector("#btn-cancel-task")?.addEventListener("click", () => {
+ showCancelTaskConfirmDialog(task);
+ });
+
+ // 提交成果
+ container.querySelector("#btn-submit")?.addEventListener("click", () => {
+ showSubmitDialog(task, currentUser);
+ });
+
+ // 验收通过
+ container.querySelector("#btn-accept")?.addEventListener("click", () => {
+ showAcceptConfirmDialog(task, currentUser);
+ });
+
+ // 验收不通过
+ container.querySelector("#btn-reject")?.addEventListener("click", () => {
+ showRejectDialog(task, currentUser);
+ });
+
+ // 发起申诉(增强版:弹窗式,支持上传证据)
+ container.querySelector("#btn-dispute")?.addEventListener("click", () => {
+ showDisputeDialog(task, currentUser);
+ });
+
+ // 查看申诉详情
+ container.querySelector("#btn-view-dispute")?.addEventListener("click", () => {
+ import("./申诉详情组件.js").then(module => {
+ const view = module.createDisputeDetailView(task.dispute_id, currentUser, () => {
+ window.dispatchEvent(new CustomEvent("comfy-route-back"));
+ });
+ window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
+ });
+ });
+}
+
+/**
+ * 📤 显示提交成果对话框
+ */
+function showSubmitDialog(task, currentUser) {
+ const content = document.createElement("div");
+ content.style.color = "#ccc";
+ content.innerHTML = `
+
+
+
+
${t('task.support_multiple_images')}
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ content.querySelector("#btn-cancel-modal").onclick = () => globalModal.closeTopModal();
+
+ content.querySelector("#btn-confirm-submit").onclick = async () => {
+ const files = content.querySelector("#deliverable-files").files;
+ const note = content.querySelector("#submit-note").value;
+
+ if (!files || files.length === 0) {
+ showToast(t('task.please_upload_deliverables'), "warning");
+ return;
+ }
+
+ try {
+ const confirmBtn = content.querySelector("#btn-confirm-submit");
+ confirmBtn.disabled = true;
+ confirmBtn.textContent = `⏳ ${t('common.uploading')}...`;
+
+ // 上传交付图片
+ const deliverables = [];
+ for (let i = 0; i < files.length; i++) {
+ confirmBtn.textContent = `⏳ ${t('common.upload_progress', { current: i + 1, total: files.length })}...`;
+ try {
+ const processedFile = await compressImageForUpload(files[i]);
+ const res = await api.uploadFile(processedFile, "task");
+ deliverables.push(res.url);
+ } catch (uploadErr) {
+ console.error("图片上传失败:", uploadErr);
+ }
+ }
+
+ if (deliverables.length === 0) {
+ showToast(t('task.upload_failed_retry'), "error");
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = t('common.confirm_submit');
+ return;
+ }
+
+ // 提交成果
+ confirmBtn.textContent = `⏳ ${t('common.submitting')}...`;
+ await api.submitTask(task.id, deliverables, note || null);
+
+ showToast(t('task.submit_success_waiting'), "success");
+ globalModal.closeTopModal();
+ // 重新加载任务详情
+ const container = document.querySelector('#task-content')?.parentElement;
+ if (container && currentUser) {
+ loadTaskDetail(container, task.id, currentUser);
+ }
+ } catch (err) {
+ showToast(err.message || t('task.submit_failed'), "error");
+ const confirmBtn = content.querySelector("#btn-confirm-submit");
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = t('common.confirm_submit');
+ }
+ };
+
+ globalModal.openModal(`📤 ${t('task.submit_work')}`, content, { width: "450px" });
+}
+
+/**
+ * 🔗 绑定任务互动事件(点赞/收藏/打赏)- 使用公共工具函数
+ */
+function bindTaskInteractionEvents(container, task, currentUser) {
+ const btnLike = container.querySelector("#btn-like");
+ const btnFavorite = container.querySelector("#btn-favorite");
+ const btnTip = container.querySelector("#btn-tip");
+ const likeCount = container.querySelector("#like-count");
+ const favoriteCount = container.querySelector("#favorite-count");
+
+ // 点赞 - 使用公共工具函数
+ if (btnLike) {
+ btnLike.onclick = () => {
+ handleToggleLike(api.toggleTaskLike, task.id, btnLike, likeCount, currentUser);
+ };
+ }
+
+ // 收藏 - 使用公共工具函数
+ if (btnFavorite) {
+ btnFavorite.onclick = () => {
+ handleToggleFavorite(api.toggleTaskFavorite, task.id, btnFavorite, favoriteCount, currentUser);
+ };
+ }
+
+ // 打赏
+ if (btnTip) {
+ btnTip.onclick = () => {
+ if (!currentUser) {
+ showToast(t('auth.login_required'), "warning");
+ return;
+ }
+ if (currentUser.account === task.publisher) {
+ showToast(t('post.tip_self'), "warning");
+ return;
+ }
+ showTaskTipDialog(task, currentUser, container);
+ };
+ }
+}
+
+/**
+ * 🎁 显示任务打赏对话框
+ */
+function showTaskTipDialog(task, currentUser, container) {
+ const content = document.createElement("div");
+ content.style.color = "#ccc";
+ content.innerHTML = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ globalModal.openModal(`🎁 ${t('post.tip_dialog_title')}`, content, { width: "300px" });
+
+ // 取消按钮
+ content.querySelector("#tip-cancel").onclick = () => globalModal.closeTopModal();
+
+ // 打赏金额按钮
+ content.querySelectorAll(".tip-amount").forEach(btn => {
+ btn.onclick = async () => {
+ const amount = parseInt(btn.dataset.amount);
+ try {
+ await api.tipTask(task.id, amount, false);
+ showToast(t('post.tip_success', { amount }), "success");
+ globalModal.closeTopModal();
+ // 刷新页面
+ loadTaskDetail(container.closest("div"), task.id, currentUser);
+ } catch (err) {
+ showToast(t('post.tip_failed') + ": " + err.message, "error");
+ }
+ };
+ });
+}
+
+/**
+ * 🎁 渲染打赏榜单 - 使用公共工具函数
+ */
+function renderTipBoard(tipBoard) {
+ return renderCommonTipBoardHTML(tipBoard, 5, t('post.no_tips'));
+}
+
+/**
+ * 🖼️ 渲染评论头像HTML
+ */
+function _renderCommentAvatar(name, avatarUrl) {
+ const initial = (name || 'U')[0].toUpperCase();
+ return avatarUrl
+ ? `

`
+ : `
${initial}
`;
+}
+
+/**
+ * 💬 加载任务评论
+ */
+async function loadTaskComments(container, taskId, currentUser) {
+ const commentsList = container.querySelector("#comments-list");
+ const commentInput = container.querySelector("#comment-input");
+ const sendBtn = container.querySelector("#btn-send-comment");
+
+ try {
+ const res = await api.getTaskComments(taskId);
+ let comments = res.data || [];
+ comments = proxyImages(comments);
+
+ if (comments.length === 0) {
+ commentsList.innerHTML = `
+
+ ${t('post.no_comments')}
+
+ `;
+ } else {
+ // 🚀 SWR 头像渲染:评论作者头像
+ commentsList.innerHTML = comments.map((c, idx) => {
+ const cached = getCachedProfile(c.author);
+ const avatar = cached?.avatar || c.author_avatar || '';
+ const name = cached?.name || c.author_name || c.author || '';
+ const avatarHtml = _renderCommentAvatar(name, avatar);
+
+ // 判断是否有删除权限(评论作者/任务发布者/管理员)
+ const currentAccount = typeof currentUser === 'string' ? currentUser : currentUser?.account;
+ const canDelete = currentUser && (
+ currentAccount === c.author ||
+ currentAccount === container.dataset?.publisher ||
+ isAdmin(currentAccount)
+ );
+
+ return `
+
+ `;
+ }).join("");
+
+ // 后台静默校对每个评论作者的头像
+ comments.forEach((c, idx) => {
+ getProfileWithSWR(c.author, api.getUserProfile, (profile) => {
+ const commentEl = commentsList.querySelector(`#task-comment-${idx}`);
+ if (!commentEl) return;
+ const avatarContainer = commentEl.querySelector('.swr-avatar-container');
+ const nameEl = commentEl.querySelector('.swr-name');
+
+ if (avatarContainer && profile.avatar) {
+ avatarContainer.innerHTML = _renderCommentAvatar(profile.name || name, profile.avatar);
+ }
+ if (nameEl && profile.name) nameEl.textContent = profile.name;
+ });
+ });
+
+ // 绑定删除评论按钮事件
+ commentsList.querySelectorAll(".btn-delete-comment").forEach(btn => {
+ btn.onclick = () => {
+ const idx = parseInt(btn.dataset.idx);
+ const comment = comments[idx];
+ if (!comment) return;
+ showDeleteCommentConfirmDialog(taskId, comment, container, currentUser);
+ };
+ });
+ }
+ } catch (err) {
+ commentsList.innerHTML = `
+
+ ${t('post.comment_load_failed')}
+
+ `;
+ }
+
+ // 发送评论
+ if (sendBtn) {
+ sendBtn.onclick = async () => {
+ if (!currentUser) {
+ showToast(t('auth.login_required'), "warning");
+ return;
+ }
+ const content = commentInput.value.trim();
+ if (!content) {
+ showToast(t('post.comment_required'), "warning");
+ return;
+ }
+ try {
+ sendBtn.disabled = true;
+ sendBtn.textContent = t('post.comment_sending');
+ await api.addTaskComment(taskId, content);
+ commentInput.value = "";
+ showToast(t('post.comment_success'), "success");
+ loadTaskComments(container, taskId, currentUser);
+ } catch (err) {
+ showToast(t('post.comment_failed') + ": " + err.message, "error");
+ } finally {
+ sendBtn.disabled = false;
+ sendBtn.textContent = t('common.send');
+ }
+ };
+ }
+
+ // 回车发送
+ if (commentInput) {
+ commentInput.onkeydown = (e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ sendBtn?.click();
+ }
+ };
+ }
+}
+
+/**
+ * 🕐 格式化时间
+ */
+function formatTime(timestamp) {
+ if (!timestamp) return "";
+ const date = new Date(timestamp * 1000);
+ return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
+}
+
+/**
+ * 📅 格式化截止日期
+ */
+function formatDeadline(deadline) {
+ if (!deadline) return t('time.unlimited');
+ try {
+ const date = new Date(deadline);
+ return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
+ } catch {
+ return deadline;
+ }
+}
+
+/**
+ * 🗑️ 显示删除任务确认对话框
+ */
+function showDeleteTaskConfirm(task, contentEl) {
+ const content = document.createElement("div");
+ content.style.color = "#ccc";
+ content.innerHTML = `
+
+
⚠️
+
${t('task.delete_confirm_title')}
+
${t('task.delete_confirm_desc')}
+
+
+
+
+
+ `;
+
+ content.querySelector("#delete-cancel").onclick = () => globalModal.closeTopModal();
+
+ content.querySelector("#delete-confirm").onclick = async () => {
+ try {
+ const confirmBtn = content.querySelector("#delete-confirm");
+ confirmBtn.disabled = true;
+ confirmBtn.textContent = t('common.deleting');
+
+ await api.cancelTask(task.id);
+ showToast(t('task.delete_success'), "success");
+ globalModal.closeTopModal();
+
+ window.dispatchEvent(new CustomEvent("comfy-route-back"));
+ } catch (err) {
+ showToast(err.message || t('task.delete_failed'), "error");
+ const confirmBtn = content.querySelector("#delete-confirm");
+ confirmBtn.disabled = false;
+ confirmBtn.textContent = t('common.delete');
+ }
+ };
+
+ globalModal.openModal(t('common.delete'), content, { width: "300px" });
+}
+
+/**
+ * ⚖️ 显示申诉对话框(支持上传证据)
+ */
+function showDisputeDialog(task, currentUser) {
+ const content = document.createElement("div");
+ content.style.color = "#ccc";
+ content.innerHTML = `
+
+
+
+
+
+
+
+
+ 📷 ${t('dispute.click_upload_evidence')}
+
+
+
+
+
+
+
+ ⚠️ ${t('dispute.notice_title')}:
+
+ - ${t('dispute.notice_1')}
+ - ${t('dispute.notice_2')}
+ - ${t('dispute.notice_3')}
+
+
+
+
+
+ `;
+
+ // 上传逻辑
+ const uploadArea = content.querySelector("#evidence-upload-area");
+ const fileInput = content.querySelector("#evidence-file-input");
+ const preview = content.querySelector("#evidence-preview");
+ const evidenceFiles = [];
+
+ uploadArea.onclick = () => fileInput.click();
+ uploadArea.onmouseover = () => { uploadArea.style.borderColor = "#2196F3"; uploadArea.style.color = "#2196F3"; };
+ uploadArea.onmouseout = () => { uploadArea.style.borderColor = "#444"; uploadArea.style.color = "#888"; };
+
+ fileInput.onchange = (e) => {
+ for (const file of e.target.files) {
+ if (evidenceFiles.length >= 6) {
+ showToast(t('dispute.max_6_images'), "warning");
+ break;
+ }
+ evidenceFiles.push(file);
+ }
+ renderPreview();
+ };
+
+ function renderPreview() {
+ preview.innerHTML = evidenceFiles.map((file, idx) => `
+
+
})
+
+
+ `).join("");
+
+ preview.querySelectorAll("button").forEach(btn => {
+ btn.onclick = () => {
+ evidenceFiles.splice(parseInt(btn.dataset.idx), 1);
+ renderPreview();
+ };
+ });
+ }
+
+ // 提交申诉
+ content.querySelector("#btn-submit-dispute").onclick = async () => {
+ const reason = content.querySelector("#dispute-reason").value.trim();
+ if (!reason) {
+ showToast(t('dispute.please_enter_reason'), "warning");
+ return;
+ }
+
+ const btn = content.querySelector("#btn-submit-dispute");
+ btn.disabled = true;
+ btn.textContent = `•• ${t('common.submitting')}...`;
+
+ try {
+ // 上传证据图片
+ const evidenceUrls = [];
+ for (let i = 0; i < evidenceFiles.length; i++) {
+ btn.textContent = `•• ${t('common.upload_image_progress', { current: i + 1, total: evidenceFiles.length })}...`;
+ const processedFile = await compressImageForUpload(evidenceFiles[i]);
+ const res = await api.uploadFile(processedFile, "dispute");
+ evidenceUrls.push(res.url);
+ }
+
+ // 提交申诉
+ btn.textContent = `•• ${t('dispute.submitting')}...`;
+ await api.disputeTask(task.id, reason, evidenceUrls);
+
+ showToast(t('dispute.submit_success'), "success");
+ globalModal.closeTopModal();
+ // 重新加载任务详情
+ const container = document.querySelector('#task-content')?.parentElement;
+ if (container && currentUser) {
+ loadTaskDetail(container, task.id, currentUser);
+ }
+ } catch (err) {
+ showToast(err.message || t('dispute.submit_failed'), "error");
+ btn.disabled = false;
+ btn.textContent = t('dispute.submit');
+ }
+ };
+
+ globalModal.openModal(`⚖️ ${t('dispute.start')}`, content, { width: "480px" });
+}
diff --git "a/\345\211\215\347\253\257\351\241\265\351\235\242/task/\345\217\221\345\270\203\344\273\273\345\212\241\347\273\204\344\273\266.js" "b/\345\211\215\347\253\257\351\241\265\351\235\242/task/\345\217\221\345\270\203\344\273\273\345\212\241\347\273\204\344\273\266.js"
new file mode 100644
index 0000000000000000000000000000000000000000..9272b10777a6a96feaad369eca39d579dfcce5e5
--- /dev/null
+++ "b/\345\211\215\347\253\257\351\241\265\351\235\242/task/\345\217\221\345\270\203\344\273\273\345\212\241\347\273\204\344\273\266.js"
@@ -0,0 +1,423 @@
+// 前端页面/task/发布任务组件.js
+// ==========================================
+// 📝 发布任务组件
+// ==========================================
+// 功能:发布新任务表单
+// ==========================================
+
+import { api } from "../core/网络请求API.js";
+import { showToast, showConfirm } from "../components/UI交互提示组件.js";
+import { t } from "../components/用户体验增强.js";
+import { removeCache } from "../components/性能优化工具.js";
+import { compressImageForUpload } from "../market/发布内容_提交引擎.js";
+
+/**
+ * 🚀 清除任务列表缓存
+ */
+function clearTaskListCache() {
+ removeCache('api_/api/tasks');
+ // 🔧 P1修复:任务榜组件实际使用 TasksCache_${status}_${sort} 键(非 ListCache_*),按真实键格式精准清除
+ const statuses = ['', 'open', 'in_progress', 'submitted', 'completed', 'disputed'];
+ // 任务榜排序值:latest, price, deadline, views, daily_views, likes, favorites
+ const sorts = ['latest', 'price', 'deadline', 'views', 'daily_views', 'likes', 'favorites'];
+ for (const status of statuses) {
+ for (const sort of sorts) {
+ removeCache(`TasksCache_${status}_${sort}`);
+ }
+ }
+ console.log('🗑️ 已清除任务列表缓存');
+}
+
+/**
+ * 🖼️ 处理任务图片上传
+ */
+async function _processTaskImages(selectedImages, publishBtn) {
+ const referenceImages = [];
+ for (let i = 0; i < selectedImages.length; i++) {
+ if (selectedImages[i].isExisting) {
+ referenceImages.push(selectedImages[i].dataUrl);
+ } else {
+ publishBtn.textContent = `⏳ ${t('task.upload_progress', { current: i + 1, total: selectedImages.length })}...`;
+ try {
+ const processedFile = await compressImageForUpload(selectedImages[i].file);
+ const res = await api.uploadFile(processedFile, "task");
+ referenceImages.push(res.url);
+ } catch (uploadErr) {
+ console.error("图片上传失败:", uploadErr);
+ }
+ }
+ }
+ return referenceImages;
+}
+
+/**
+ * 📝 创建发布任务视图
+ * @param {Object|string} currentUser - 当前用户
+ * @param {Object} editTaskData - 编辑模式时的任务数据(可选)
+ */
+export function createPublishTaskView(currentUser, editTaskData = null) {
+ const isEditMode = !!editTaskData;
+ const isLockedStatus = editTaskData && ["in_progress", "submitted"].includes(editTaskData.status);
+
+ const container = document.createElement("div");
+ Object.assign(container.style, {
+ display: "flex",
+ flexDirection: "column",
+ gap: "0",
+ color: "#ccc",
+ fontSize: "14px",
+ padding: "0",
+ overflowY: "auto",
+ flex: "1",
+ boxSizing: "border-box",
+ background: "#121212"
+ });
+
+ // 获取明天的日期作为最小截止日期
+ const tomorrow = new Date();
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ const minDate = tomorrow.toISOString().split("T")[0];
+
+ // 默认截止日期(7天后)或编辑模式下的原截止日期
+ let defaultDate;
+ if (isEditMode && editTaskData.deadline) {
+ const editDeadline = new Date(editTaskData.deadline);
+ defaultDate = editDeadline.toISOString().split("T")[0];
+ } else {
+ const defaultDeadline = new Date();
+ defaultDeadline.setDate(defaultDeadline.getDate() + 7);
+ defaultDate = defaultDeadline.toISOString().split("T")[0];
+ }
+
+ // 编辑模式下的字段值
+ const editTitle = isEditMode ? (editTaskData.title || '') : '';
+ const editDescription = isEditMode ? (editTaskData.description || '') : '';
+ const editRefLink = isEditMode ? (editTaskData.reference_link || '') : '';
+ const editTotalPrice = isEditMode ? (editTaskData.total_price || '') : '';
+ const editDepositRatio = isEditMode ? (editTaskData.deposit_ratio || 30) : 30;
+
+ container.innerHTML = `
+
+
+
+
📝 ${isEditMode ? (t('task.edit_task') || '编辑任务') : t('task.publish')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${editDescription.length}/2000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${t('common.credits')}
+
+
${isLockedStatus ? '⚠️ 任务进行中,价格不可修改' : t('task.min_10_credits')}
+
+
+
+
+
+
+
${isLockedStatus ? '⚠️ 任务进行中,订金比例不可修改' : t('task.deposit_deducted_on_assign')}
+
+
+
+
+
+
💡 ${t('task.price_description')}
+
+ ${t('task.total_price')}: 0 ${t('common.credits')}
+ ${t('task.deposit_on_assign')}: 0 ${t('common.credits')}
+ ${t('task.remaining_on_accept')}: 0 ${t('common.credits')}
+
+
+
+
+
+
+
+ ${isLockedStatus ? '
⚠️ 任务进行中,截止日期不可修改
' : ''}
+
+
+
+
+
+
+
+ ${t('task.manage_in_task_list')}
+
+
+ `;
+
+ // 🔌 绑定事件
+
+ // 返回按钮
+ container.querySelector("#btn-back").onclick = async () => {
+ const hasChanges = !!(
+ container.querySelector("#task-title")?.value?.trim() ||
+ container.querySelector("#task-description")?.value?.trim() ||
+ container.querySelector("#image-preview")?.children?.length > 0 ||
+ container.querySelector("#ref-link")?.value?.trim()
+ );
+
+ if (hasChanges) {
+ const confirmed = await showConfirm(
+ isEditMode ? t('publish.unsaved_changes_desc') : t('publish.leave_confirm_desc'),
+ {
+ title: isEditMode ? t('publish.unsaved_changes_title') : t('publish.leave_confirm_title'),
+ confirmText: t('common.leave'),
+ cancelText: t('common.stay'),
+ type: 'warning'
+ }
+ );
+ if (!confirmed) return;
+ }
+
+ window.dispatchEvent(new CustomEvent("comfy-route-back"));
+ };
+
+ // 描述字数统计
+ const descInput = container.querySelector("#task-description");
+ const descCount = container.querySelector("#desc-count");
+ descInput.oninput = () => {
+ descCount.textContent = descInput.value.length;
+ };
+
+ // 价格计算
+ const totalPriceInput = container.querySelector("#total-price");
+ const depositRatioSelect = container.querySelector("#deposit-ratio");
+ const summaryTotal = container.querySelector("#summary-total");
+ const summaryDeposit = container.querySelector("#summary-deposit");
+ const summaryRemaining = container.querySelector("#summary-remaining");
+
+ const updatePriceSummary = () => {
+ const total = parseInt(totalPriceInput.value) || 0;
+ const ratio = parseInt(depositRatioSelect.value) || 30;
+ const deposit = Math.floor(total * ratio / 100);
+ const remaining = total - deposit;
+
+ summaryTotal.textContent = total;
+ summaryDeposit.textContent = deposit;
+ summaryRemaining.textContent = remaining;
+ };
+
+ totalPriceInput.oninput = updatePriceSummary;
+ depositRatioSelect.onchange = updatePriceSummary;
+
+ // 图片上传
+ const refImagesInput = container.querySelector("#ref-images");
+ const imagePreview = container.querySelector("#image-preview");
+ const btnAddImage = container.querySelector("#btn-add-image");
+ let selectedImages = [];
+
+ btnAddImage.onclick = () => {
+ if (selectedImages.length >= 6) {
+ showToast(t('task.max_6_images'), "warning");
+ return;
+ }
+ refImagesInput.click();
+ };
+
+ refImagesInput.onchange = () => {
+ const files = Array.from(refImagesInput.files);
+ const remaining = 6 - selectedImages.length;
+ const toAdd = files.slice(0, remaining);
+
+ toAdd.forEach(file => {
+ if (!file.type.startsWith("image/")) return;
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ selectedImages.push({
+ file: file,
+ dataUrl: e.target.result
+ });
+ renderImagePreview();
+ };
+ reader.readAsDataURL(file);
+ });
+
+ refImagesInput.value = "";
+ };
+
+ const renderImagePreview = () => {
+ imagePreview.innerHTML = selectedImages.map((img, i) => `
+
+

+
+
+ `).join("");
+
+ imagePreview.querySelectorAll(".btn-remove-img").forEach(btn => {
+ btn.onclick = () => {
+ const index = parseInt(btn.dataset.index);
+ selectedImages.splice(index, 1);
+ renderImagePreview();
+ };
+ });
+
+ btnAddImage.style.display = selectedImages.length >= 6 ? "none" : "block";
+ };
+
+ // 编辑模式:加载已有的参考图片
+ if (isEditMode && editTaskData.reference_images && editTaskData.reference_images.length > 0) {
+ editTaskData.reference_images.forEach(url => {
+ selectedImages.push({
+ file: null,
+ dataUrl: url,
+ isExisting: true
+ });
+ });
+ renderImagePreview();
+ }
+
+ // 发布/保存按钮
+ container.querySelector("#btn-publish").onclick = async () => {
+ const title = container.querySelector("#task-title").value.trim();
+ const description = descInput.value.trim();
+ const refLink = container.querySelector("#ref-link").value.trim();
+ const totalPrice = parseInt(totalPriceInput.value) || 0;
+ const depositRatio = parseInt(depositRatioSelect.value) || 30;
+ const deadline = container.querySelector("#deadline").value;
+
+ // 验证
+ const validations = [
+ [!title, t('task.please_enter_title')],
+ [!description, t('task.please_enter_description')],
+ [!isEditMode && totalPrice < 0, t('task.price_min_10')],
+ [!deadline, t('task.please_select_deadline')]
+ ];
+ const failed = validations.find(([cond]) => cond);
+ if (failed) { showToast(failed[1], "warning"); return; }
+
+ try {
+ const publishBtn = container.querySelector("#btn-publish");
+ publishBtn.disabled = true;
+ publishBtn.textContent = `⏳ ${t('task.uploading_images')}...`;
+
+ // 上传参考图片(只上传新选择的图片)
+ const referenceImages = await _processTaskImages(selectedImages, publishBtn);
+
+ if (isEditMode) {
+ // 编辑模式:更新任务
+ publishBtn.textContent = `⏳ ${t('common.saving')}...`;
+
+ const updateData = {
+ title,
+ description,
+ referenceImages,
+ referenceLink: refLink || null
+ };
+
+ // 只有在非锁定状态下才更新价格相关字段
+ if (!isLockedStatus) {
+ updateData.totalPrice = totalPrice;
+ updateData.depositRatio = depositRatio;
+ updateData.deadline = deadline;
+ }
+
+ await api.updateTask(editTaskData.id, updateData);
+
+ showToast(t('task.edit_success') || '任务修改成功', "success");
+ } else {
+ // 新建模式:创建任务
+ publishBtn.textContent = `⏳ ${t('task.publishing')}...`;
+
+ await api.createTask({
+ title,
+ description,
+ referenceImages,
+ referenceLink: refLink || null,
+ totalPrice,
+ depositRatio,
+ deadline,
+ publisher: currentUser.account || currentUser
+ });
+
+ showToast(t('task.publish_success'), "success");
+ }
+
+ // 🚀 清除任务列表缓存,确保返回列表时能看到更新
+ clearTaskListCache();
+ // 🔄 触发列表刷新,确保新内容立即显示
+ window.dispatchEvent(new CustomEvent("comfy-trigger-sidebar-reload"));
+
+ window.dispatchEvent(new CustomEvent("comfy-route-back"));
+
+ } catch (err) {
+ console.error(isEditMode ? "修改任务失败:" : "发布任务失败:", err);
+ showToast(err.message || (isEditMode ? t('task.edit_failed') || '修改失败' : t('task.publish_failed')), "error");
+ } finally {
+ const publishBtn = container.querySelector("#btn-publish");
+ publishBtn.disabled = false;
+ publishBtn.textContent = `🚀 ${isEditMode ? t('task.save_changes') : t('task.publish_task')}`;
+ }
+ };
+
+ return container;
+}
diff --git "a/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\224\263\350\257\211\350\257\246\346\203\205\347\273\204\344\273\266.js" "b/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\224\263\350\257\211\350\257\246\346\203\205\347\273\204\344\273\266.js"
new file mode 100644
index 0000000000000000000000000000000000000000..e5c9d209c3e2662bb15b664412a176c491eccba1
--- /dev/null
+++ "b/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\224\263\350\257\211\350\257\246\346\203\205\347\273\204\344\273\266.js"
@@ -0,0 +1,271 @@
+// 前端页面/task/申诉详情组件.js
+// ==========================================
+// ⚖️ 申诉详情页面组件
+// ==========================================
+// 功能:展示申诉双方举证、回应、仲裁结果
+// ==========================================
+
+import { api } from "../core/网络请求API.js";
+import { showToast } from "../components/UI交互提示组件.js";
+import { t, getLanguage } from "../components/用户体验增强.js";
+import { isAdmin, IS_WEB_MODE } from "../core/全局配置.js";
+
+/**
+ * 创建申诉详情视图
+ * @param {string} disputeId - 申诉ID
+ * @param {object} currentUser - 当前用户
+ * @param {function} onBack - 返回回调
+ */
+export function createDisputeDetailView(disputeId, currentUser, onBack) {
+ const container = document.createElement("div");
+ container.className = "dispute-detail-container";
+ // 🔧 自适应高度:flex 填充剩余空间,内容超出时在容器内部滚动(与主界面保持一致)
+ container.style.cssText = "padding: 16px; background: var(--comfy-menu-bg); flex: 1; min-height: 0; overflow-y: auto;";
+
+ container.innerHTML = `
⏳ ${t('common.loading')}
`;
+
+ loadDisputeDetail(container, disputeId, currentUser, onBack);
+
+ return container;
+}
+
+async function loadDisputeDetail(container, disputeId, currentUser, onBack) {
+ try {
+ const res = await api.getDisputeDetail(disputeId);
+ if (res.status !== "success") throw new Error(t('dispute.get_detail_failed'));
+
+ const dispute = res.data;
+ renderDisputeDetail(container, dispute, currentUser, onBack);
+ } catch (err) {
+ container.innerHTML = `
❌ ${err.message}
`;
+ }
+}
+
+function renderDisputeDetail(container, dispute, currentUser, onBack) {
+ const isInitiator = currentUser?.account === dispute.initiator;
+ const isRespondent = currentUser?.account === (dispute.initiator_role === "publisher" ? dispute.assignee : dispute.publisher);
+ const isAdminUser = isAdmin(currentUser?.account);
+ const canRespond = isRespondent && dispute.status === "pending";
+
+ const statusConfig = {
+ pending: { label: t('dispute.status_pending'), color: "#FF9800", bg: "rgba(255, 152, 0, 0.1)" },
+ responded: { label: t('dispute.status_responded'), color: "#2196F3", bg: "rgba(33, 150, 243, 0.1)" },
+ resolved: { label: t('dispute.status_resolved'), color: "#4CAF50", bg: "rgba(76, 175, 80, 0.1)" }
+ };
+ const statusInfo = statusConfig[dispute.status] || statusConfig.pending;
+
+ container.innerHTML = `
+
+
+
+
+
+
+
📋 ${t('dispute.related_task')}
+
${dispute.task_title || t('common.unknown_task')}
+
${t('dispute.task_id')}: ${dispute.task_id}
+
+
+
+
+
👥 ${t('dispute.parties')}
+
+
})
+
+
${dispute.publisher_name || dispute.publisher}
+
${t('dispute.publisher')} ${dispute.initiator_role === "publisher" ? `(${t('dispute.initiator')})` : `(${t('dispute.respondent')})`}
+
+
+
+
})
+
+
${dispute.assignee_name || dispute.assignee}
+
${t('task.assignee')} ${dispute.initiator_role === "assignee" ? `(${t('dispute.initiator')})` : `(${t('dispute.respondent')})`}
+
+
+
+
+
+
+
📝 ${t('dispute.initiator_statement')}
+
+
${dispute.reason || t('common.none')}
+ ${_renderEvidenceGallery(dispute.evidence)}
+
${t('dispute.submitted_at')} ${formatTime(dispute.created_at)}
+
+
+
+
+
+
💬 ${t('dispute.respondent_response')}
+ ${dispute.response ? `
+
+
${dispute.response}
+ ${_renderEvidenceGallery(dispute.response_evidence)}
+
${t('dispute.responded_at')} ${formatTime(dispute.responded_at)}
+
+ ` : `
+
${t('dispute.no_response')}
+ ${canRespond ? `
+
+ ` : ""}
+ `}
+
+
+
+ ${dispute.status === "resolved" ? `
+
+
⚖️ ${t('dispute.resolution_result')}
+
${getResolutionText(dispute.resolution, dispute.resolution_ratio)}
+ ${dispute.resolution_note ? `
+
${dispute.resolution_note}
+ ` : ""}
+
${t('dispute.resolved_at')} ${formatTime(dispute.resolved_at)}
+
+ ` : ""}
+ `;
+
+ // 返回按钮
+ container.querySelector("#backBtn").onclick = () => onBack && onBack();
+
+ // 回应表单逻辑
+ if (canRespond) {
+ const uploadedEvidence = [];
+ const uploadArea = container.querySelector("#uploadArea");
+ const fileInput = container.querySelector("#fileInput");
+ const uploadedImages = container.querySelector("#uploadedImages");
+ const submitBtn = container.querySelector("#submitResponseBtn");
+
+ uploadArea.onclick = () => fileInput.click();
+ fileInput.onchange = async (e) => {
+ for (const file of e.target.files) {
+ if (uploadedEvidence.length >= 6) {
+ showToast(t('dispute.max_6_images'), "warning");
+ break;
+ }
+ try {
+ const res = await api.uploadFile(file, "dispute");
+ uploadedEvidence.push(res.url);
+ renderUploadedImages();
+ } catch (err) {
+ showToast(t('dispute.upload_failed') + ": " + err.message, "error");
+ }
+ }
+ };
+
+ function renderUploadedImages() {
+ uploadedImages.innerHTML = uploadedEvidence.map((url, idx) => `
+
+

+
+
+ `).join("");
+ uploadedImages.querySelectorAll(".remove-btn").forEach(btn => {
+ btn.onclick = () => {
+ uploadedEvidence.splice(parseInt(btn.dataset.idx), 1);
+ renderUploadedImages();
+ };
+ });
+ }
+
+ submitBtn.onclick = async () => {
+ const responseText = container.querySelector("#respondText").value.trim();
+ if (!responseText) {
+ showToast(t('dispute.please_enter_response'), "warning");
+ return;
+ }
+ submitBtn.disabled = true;
+ submitBtn.textContent = `${t('common.submitting')}...`;
+ try {
+ await api.respondDispute(dispute.id, responseText, uploadedEvidence);
+ showToast(t('dispute.response_submitted'), "success");
+ loadDisputeDetail(container, dispute.id, currentUser, onBack);
+ } catch (err) {
+ showToast(t('dispute.submit_failed') + ": " + err.message, "error");
+ submitBtn.disabled = false;
+ submitBtn.textContent = t('dispute.submit_response');
+ }
+ };
+ }
+}
+
+function getResolutionText(resolution, ratio) {
+ if (resolution === "favor_initiator") return t('dispute.favor_initiator');
+ if (resolution === "favor_respondent") return t('dispute.favor_respondent');
+ if (resolution === "split") return `${t('dispute.split_result')} (${t('dispute.initiator')} ${ratio}% : ${t('dispute.respondent')} ${100 - ratio}%)`;
+ return t('dispute.resolved');
+}
+
+function formatTime(timestamp) {
+ if (!timestamp) return t('common.unknown');
+ const date = new Date(timestamp * 1000);
+ return date.toLocaleString(getLanguage(), { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
+}
+
+/**
+ * 渲染证据图片画廊
+ * @param {string[]} images - 证据图片URL数组
+ * @returns {string} HTML字符串
+ */
+function _renderEvidenceGallery(images) {
+ if (!images || images.length === 0) return "";
+ return `
+ ${images.map(img => `

`).join("")}
+
`;
+}
diff --git "a/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\256\241\347\220\206\345\221\230\344\273\262\350\243\201\347\273\204\344\273\266.js" "b/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\256\241\347\220\206\345\221\230\344\273\262\350\243\201\347\273\204\344\273\266.js"
new file mode 100644
index 0000000000000000000000000000000000000000..ffc96a917cd34466a7a53f8d9cde4a2bf84e5aa6
--- /dev/null
+++ "b/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\256\241\347\220\206\345\221\230\344\273\262\350\243\201\347\273\204\344\273\266.js"
@@ -0,0 +1,505 @@
+// 前端页面/task/管理员仲裁组件.js
+// ==========================================
+// ⚖️ 管理员仲裁界面
+// ==========================================
+// 功能:查看申诉列表、进行裁决
+// ==========================================
+
+import { api } from "../core/网络请求API.js";
+import { showToast } from "../components/UI交互提示组件.js";
+import { t, getLanguage } from "../components/用户体验增强.js";
+import { globalModal } from "../components/全局弹窗管理器.js";
+import { setCache, getCache, removeCache, createSkeleton } from "../components/性能优化工具.js";
+
+const escapeHtml = (str) => {
+ if (!str) return '';
+ return String(str).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+};
+
+// 缓存配置
+const CACHE_KEY = "AdminDisputesCache";
+function getCacheTTL() {
+ try {
+ const s = localStorage.getItem('ComfyCommunity_Settings');
+ if (s) { const v = parseInt(JSON.parse(s).cacheExpireSeconds); if (v >= 60 && v <= 86400) return v * 1000; }
+ } catch(e) {}
+ return 1000 * 60 * 30; // 默认30分钟
+}
+
+// 全局状态:缓存所有争议数据
+let allDisputesCache = [];
+let isLoadingFromNetwork = false;
+
+// 状态配置常量(labelKey用于延迟翻译,保证i18n正确性)
+const STATUS_CONFIG = {
+ pending: { labelKey: 'arbitrate.pending_response', color: "#FF9800", bg: "rgba(255, 152, 0, 0.15)" },
+ responded: { labelKey: 'arbitrate.pending_ruling', color: "#2196F3", bg: "rgba(33, 150, 243, 0.15)" },
+ resolved: { labelKey: 'arbitrate.ruled', color: "#4CAF50", bg: "rgba(76, 175, 80, 0.15)" }
+};
+
+// 裁决选项配置
+const RESOLUTION_OPTIONS = [
+ { value: "favor_initiator", icon: "\u2705", labelKey: 'dispute.favor_initiator' },
+ { value: "favor_respondent", icon: "\u274C", labelKey: 'dispute.favor_respondent' },
+ { value: "split", icon: "\u2696\uFE0F", labelKey: 'arbitrate.negotiate_split', descKey: 'arbitrate.split_desc' }
+];
+
+function _renderEvidenceRow(images) {
+ if (!images || images.length === 0) return "";
+ return `
+ ${images.map(img => `
})
`).join("")}
+
`;
+}
+
+/**
+ * 创建管理员仲裁视图
+ * @param {object} currentUser - 当前管理员用户
+ */
+export function createAdminDisputeView(currentUser) {
+ const container = document.createElement("div");
+ container.className = "admin-dispute-container";
+ // 🔧 自适应高度:flex 填充剩余空间,内容超出时在容器内部滚动(与主界面保持一致)
+ container.style.cssText = "padding: 16px; background: var(--comfy-menu-bg); flex: 1; min-height: 0; overflow-y: auto;";
+
+ renderDisputeList(container, currentUser);
+
+ return container;
+}
+
+async function renderDisputeList(container, currentUser, statusFilter = null) {
+ // 渲染UI框架
+ container.innerHTML = `
+
+
+
+
+
+
+
+
⏳ ${t('common.loading')}
+
+ `;
+
+ const listEl = container.querySelector("#disputeList");
+ const cacheIndicator = container.querySelector("#cacheIndicator");
+
+ // 返回按钮事件 — 返回管理后台
+ container.querySelector("#btn-back-arbitrate").onclick = () => {
+ import("../profile/管理后台组件.js").then(module => {
+ const view = module.createAdminPanelView(currentUser);
+ window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
+ }).catch(err => {
+ console.error('加载失败:', err);
+ window.dispatchEvent(new CustomEvent("comfy-route-back"));
+ });
+ };
+
+ // Tab切换
+ container.querySelectorAll(".admin-tab").forEach(tab => {
+ tab.onclick = () => {
+ const status = tab.dataset.status || null;
+ renderDisputeList(container, currentUser, status);
+ };
+ });
+
+ // 🚀 缓存优先策略:尝试从缓存加载
+ const cachedData = getCache(CACHE_KEY);
+
+ if (cachedData && cachedData.length > 0) {
+ // 有缓存,先显示缓存数据
+ allDisputesCache = cachedData;
+ const filteredDisputes = filterDisputesByStatus(allDisputesCache, statusFilter);
+ renderDisputeCards(listEl, filteredDisputes, currentUser, statusFilter);
+ cacheIndicator.textContent = "📦 缓存";
+
+ // 后台静默刷新
+ silentRefreshDisputes(listEl, currentUser, statusFilter, cacheIndicator);
+ } else {
+ // 无缓存,显示骨架屏并加载数据
+ listEl.innerHTML = "";
+ listEl.appendChild(createSkeleton("list", 4));
+ await loadDisputesFromNetwork(listEl, currentUser, statusFilter, cacheIndicator);
+ }
+}
+
+/**
+ * 按状态过滤争议数据
+ */
+function filterDisputesByStatus(disputes, statusFilter) {
+ if (!statusFilter) return disputes;
+ return disputes.filter(d => d.status === statusFilter);
+}
+
+/**
+ * 渲染争议卡片列表
+ */
+function renderDisputeCards(listEl, disputes, currentUser, statusFilter) {
+ if (disputes.length === 0) {
+ listEl.innerHTML = `
+
+
📭
+
${t('arbitrate.no_disputes')}
+
+ `;
+ return;
+ }
+
+ listEl.innerHTML = disputes.map(d => {
+ const status = STATUS_CONFIG[d.status] || STATUS_CONFIG.pending;
+ return `
+
+
+
+ ${escapeHtml(d.publisher_name || d.publisher)}
+ VS
+ ${escapeHtml(d.assignee_name || d.assignee)}
+
+
${escapeHtml(d.reason) || t('arbitrate.no_reason')}
+
+ ${t('arbitrate.initiator')}: ${d.initiator_role === "publisher" ? t('dispute.publisher') : t('task.assignee')}
+ ${formatTime(d.created_at)}
+
+
+ `;
+ }).join("");
+
+ // 点击进入详情
+ listEl.querySelectorAll(".dispute-card").forEach(card => {
+ card.onclick = () => {
+ const disputeId = card.dataset.id;
+ showDisputeModal(disputeId, currentUser, () => renderDisputeList(listEl.parentElement, currentUser, statusFilter));
+ };
+ });
+}
+
+/**
+ * 从网络加载争议数据
+ */
+async function loadDisputesFromNetwork(listEl, currentUser, statusFilter, cacheIndicator) {
+ if (isLoadingFromNetwork) return;
+
+ try {
+ isLoadingFromNetwork = true;
+
+ // 始终获取全部数据(不传statusFilter)
+ const res = await api.getAdminDisputes(null);
+ const disputes = res.data || [];
+
+ // 更新全局缓存
+ allDisputesCache = disputes;
+
+ // 保存到本地缓存
+ setCache(CACHE_KEY, disputes, getCacheTTL(), true);
+
+ // 更新缓存指示器
+ if (cacheIndicator) cacheIndicator.textContent = "";
+
+ // 按当前筛选状态过滤并渲染
+ const filteredDisputes = filterDisputesByStatus(disputes, statusFilter);
+ renderDisputeCards(listEl, filteredDisputes, currentUser, statusFilter);
+
+ } catch (err) {
+ console.error("加载申诉列表失败:", err);
+ listEl.innerHTML = `
+
❌ ${t('common.load_failed')}: ${err.message}
+ `;
+ } finally {
+ isLoadingFromNetwork = false;
+ }
+}
+
+/**
+ * SWR模式:后台静默刷新
+ */
+async function silentRefreshDisputes(listEl, currentUser, statusFilter, cacheIndicator) {
+ if (isLoadingFromNetwork) return;
+
+ try {
+ isLoadingFromNetwork = true;
+
+ // 获取最新全部数据
+ const res = await api.getAdminDisputes(null);
+ const freshDisputes = res.data || [];
+
+ // 检查数据是否有变化
+ if (_disputesDataChanged(allDisputesCache, freshDisputes)) {
+ console.log("✅ 仲裁中心检测到新数据,执行静默更新");
+
+ // 更新缓存
+ allDisputesCache = freshDisputes;
+ setCache(CACHE_KEY, freshDisputes, getCacheTTL(), true);
+
+ // 重新渲染当前筛选状态的数据
+ const filteredDisputes = filterDisputesByStatus(freshDisputes, statusFilter);
+ renderDisputeCards(listEl, filteredDisputes, currentUser, statusFilter);
+
+ // 短暂显示更新提示
+ if (cacheIndicator) {
+ cacheIndicator.textContent = "✓ 已更新";
+ setTimeout(() => {
+ if (cacheIndicator) cacheIndicator.textContent = "";
+ }, 2000);
+ }
+ }
+ } catch (err) {
+ console.warn("后台刷新失败:", err);
+ } finally {
+ isLoadingFromNetwork = false;
+ }
+}
+
+/**
+ * 对比争议数据是否有变化
+ */
+function _disputesDataChanged(oldData, newData) {
+ if (!oldData || !newData) return true;
+ if (oldData.length !== newData.length) return true;
+
+ // 检查前10条数据的关键字段
+ const checkCount = Math.min(10, oldData.length);
+ for (let i = 0; i < checkCount; i++) {
+ if (oldData[i].id !== newData[i].id) return true;
+ if (oldData[i].status !== newData[i].status) return true;
+ }
+ return false;
+}
+
+function showDisputeModal(disputeId, currentUser, onClose) {
+ // 创建初始加载内容
+ const content = document.createElement("div");
+ content.style.cssText = "padding: 40px; text-align: center; color: #888;";
+ content.innerHTML = `⏳ ${t('common.loading')}`;
+
+ // 打开弹窗
+ globalModal.openModal(`⚖️ ${t('dispute.title')}`, content, { width: "600px" });
+
+ // 异步加载申诉详情内容
+ loadDisputeModalContent(content, disputeId, currentUser, () => {
+ globalModal.closeTopModal();
+ onClose && onClose();
+ });
+}
+
+async function loadDisputeModalContent(content, disputeId, currentUser, onClose) {
+ content.innerHTML = `
⏳ ${t('common.loading')}
`;
+
+ try {
+ const res = await api.getDisputeDetail(disputeId);
+ const dispute = res.data;
+
+ const canResolve = dispute.status !== "resolved";
+
+ content.innerHTML = `
+
+
+
+
+
📋 ${t('dispute.related_task')}
+
${dispute.task_title || t('common.unknown_task')}
+
+
+
+
📝 ${t('dispute.initiator_statement')} (${dispute.initiator_role === "publisher" ? t('dispute.publisher') : t('task.assignee')})
+
+ ${escapeHtml(dispute.reason) || t('common.none')}
+ ${_renderEvidenceRow(dispute.evidence)}
+
+
+
+
+
💬 ${t('dispute.respondent_response')} (${dispute.initiator_role === "publisher" ? t('task.assignee') : t('dispute.publisher')})
+
+ ${escapeHtml(dispute.response) || `(${t('dispute.no_response')})`}
+ ${_renderEvidenceRow(dispute.response_evidence)}
+
+
+
+ ${canResolve ? `
+
+
🔨 ${t('arbitrate.ruling_operation')}
+
+
+ ${RESOLUTION_OPTIONS.map(opt => {
+ const desc = opt.descKey
+ ? t(opt.descKey)
+ : (opt.value === "favor_initiator"
+ ? (dispute.initiator_role === "publisher" ? t('arbitrate.refund_publisher') : t('arbitrate.assignee_full_pay'))
+ : (dispute.initiator_role === "publisher" ? t('arbitrate.assignee_full_pay') : t('arbitrate.refund_publisher')));
+ return `
`;
+ }).join("")}
+
+
+
+
+
+ 50% : 50%
+
+
+
+
+
+
+
+
+ ` : `
+
+
✅ ${t('dispute.resolution_result')}
+
+
${getResolutionText(dispute.resolution, dispute.resolution_ratio)}
+ ${dispute.resolution_note ? `
${escapeHtml(dispute.resolution_note)}
` : ""}
+
+
+ `}
+
+ `;
+
+ // 裁决逻辑
+ if (canResolve) {
+ let selectedResolution = null;
+ let ratio = 50;
+
+ const options = content.querySelectorAll(".resolve-option");
+ const splitRatioEl = content.querySelector("#splitRatio");
+ const ratioSlider = content.querySelector("#ratioSlider");
+ const ratioValue = content.querySelector("#ratioValue");
+ const resolveBtn = content.querySelector("#resolveBtn");
+
+ options.forEach(opt => {
+ opt.onclick = () => {
+ options.forEach(o => o.classList.remove("selected"));
+ opt.classList.add("selected");
+ opt.querySelector("input").checked = true;
+ selectedResolution = opt.dataset.value;
+ splitRatioEl.style.display = selectedResolution === "split" ? "flex" : "none";
+ resolveBtn.disabled = false;
+ };
+ });
+
+ ratioSlider.oninput = () => {
+ ratio = parseInt(ratioSlider.value);
+ ratioValue.textContent = `${ratio}% : ${100 - ratio}%`;
+ };
+
+ resolveBtn.onclick = async () => {
+ if (!selectedResolution) return;
+ const note = content.querySelector("#resolveNote").value.trim();
+ resolveBtn.disabled = true;
+ resolveBtn.textContent = `${t('common.processing')}...`;
+
+ try {
+ await api.resolveDispute(dispute.id, selectedResolution, selectedResolution === "split" ? ratio : null, note || null);
+ showToast(t('arbitrate.ruling_success'), "success");
+
+ // 🗑️ 裁决成功后清除缓存,强制下次重新加载
+ allDisputesCache = [];
+ removeCache(CACHE_KEY);
+
+ onClose();
+ } catch (err) {
+ showToast(t('arbitrate.ruling_failed') + ": " + err.message, "error");
+ resolveBtn.disabled = false;
+ resolveBtn.textContent = t('arbitrate.confirm_ruling');
+ }
+ };
+ }
+
+ } catch (err) {
+ content.innerHTML = `
❌ ${err.message}
`;
+ }
+}
+
+function getResolutionText(resolution, ratio) {
+ if (resolution === "favor_initiator") return t('dispute.favor_initiator');
+ if (resolution === "favor_respondent") return t('dispute.favor_respondent');
+ if (resolution === "split") return `${t('arbitrate.negotiate_split')}(${ratio}% : ${100 - ratio}%)`;
+ return t('dispute.resolved');
+}
+
+function formatTime(timestamp) {
+ if (!timestamp) return t('common.unknown');
+ const date = new Date(timestamp * 1000);
+ return date.toLocaleString(getLanguage(), { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
+}
diff --git "a/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\256\241\347\220\206\345\221\230\346\217\220\347\216\260\347\273\204\344\273\266.js" "b/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\256\241\347\220\206\345\221\230\346\217\220\347\216\260\347\273\204\344\273\266.js"
new file mode 100644
index 0000000000000000000000000000000000000000..934951d0476446c4cc63751ba7a08d2145c8472e
--- /dev/null
+++ "b/\345\211\215\347\253\257\351\241\265\351\235\242/task/\347\256\241\347\220\206\345\221\230\346\217\220\347\216\260\347\273\204\344\273\266.js"
@@ -0,0 +1,272 @@
+// 前端页面/task/管理员提现组件.js
+// ==========================================
+// 💰 管理员提现管理界面
+// ==========================================
+// 功能:查看提现列表、确认打款
+// ==========================================
+
+import { api } from "../core/网络请求API.js";
+import { showToast, showConfirm } from "../components/UI交互提示组件.js";
+import { t, getLanguage } from "../components/用户体验增强.js";
+
+/**
+ * 创建管理员提现管理视图
+ * @param {object} currentUser - 当前管理员用户
+ */
+export function createWithdrawManageView(currentUser) {
+ const container = document.createElement("div");
+ container.className = "admin-withdraw-container";
+ // 🔧 自适应高度:flex 填充剩余空间,内容超出时在容器内部滚动(与主界面保持一致)
+ container.style.cssText = "padding: 16px; background: var(--comfy-menu-bg); flex: 1; min-height: 0; overflow-y: auto;";
+
+ renderWithdrawList(container, currentUser, "pending");
+
+ return container;
+}
+
+async function renderWithdrawList(container, currentUser, statusFilter = "pending") {
+ container.innerHTML = `
+
+
+
+
+
+
+
+
⏳ ${t('common.loading')}
+
+ `;
+
+ // 返回按钮事件 — 返回管理后台
+ container.querySelector("#btn-back-withdraw").onclick = () => {
+ delete window._handleConfirmWithdrawal;
+ import("../profile/管理后台组件.js").then(module => {
+ const view = module.createAdminPanelView(currentUser);
+ window.dispatchEvent(new CustomEvent("comfy-route-view", { detail: { view } }));
+ }).catch(err => {
+ console.error('加载失败:', err);
+ window.dispatchEvent(new CustomEvent("comfy-route-back"));
+ });
+ };
+
+ // Tab切换
+ container.querySelectorAll(".admin-tab").forEach(tab => {
+ tab.onclick = () => {
+ const status = tab.dataset.status;
+ renderWithdrawList(container, currentUser, status);
+ };
+ });
+
+ // 加载提现列表
+ try {
+ const res = await api.getAdminWithdrawals(statusFilter);
+ const withdrawals = res.data || [];
+ const listEl = container.querySelector("#withdrawList");
+
+ if (withdrawals.length === 0) {
+ listEl.innerHTML = `
+
+
📭
+
${statusFilter === 'pending' ? t('withdraw.no_pending') : t('withdraw.no_completed')}
+
+ `;
+ return;
+ }
+
+ listEl.innerHTML = withdrawals.map(w => {
+ const isPending = w.withdraw_status === "pending";
+ const amount = Math.abs(w.amount || 0);
+ const createdAt = formatTime(w.created_at);
+
+ return `
+
+
+
+
+
+
📱 ${t('withdraw.alipay_account')}
+
${w.alipay_account || '-'}
+
+
+
👤 ${t('withdraw.real_name')}
+
${w.real_name || '-'}
+
+
+
+
+ 🕐 ${t('withdraw.apply_time')}: ${createdAt}
+ ID: #${w.tx_id}
+
+
+ ${isPending ? `
+
+
+
+
+ ` : `
+
+ ✅
+ ${t('withdraw.payment_completed')}
+ ${t('withdraw.order_id')}: ${w.payment_order_id || '-'}
+
+ `}
+
+ `;
+ }).join("");
+
+ // 绑定确认打款事件
+ window._handleConfirmWithdrawal = async (txId) => {
+ const inputEl = container.querySelector(`#orderInput-${txId}`);
+ const btnEl = container.querySelector(`#confirmBtn-${txId}`);
+ const paymentOrderId = inputEl.value.trim();
+
+ if (!paymentOrderId) {
+ showToast(t('withdraw.order_required'), "warning");
+ inputEl.focus();
+ return;
+ }
+
+ // 二次确认弹窗
+ const confirmMessage = `
+
+
💰
+
+ ${t('withdraw.confirm_desc')}
+
+
+
${t('withdraw.order_id')}
+
${escapeHtml(paymentOrderId)}
+
+
+ `;
+
+ // 在 showConfirm 调用前禁用按钮防止连点
+ btnEl.disabled = true;
+
+ const confirmed = await showConfirm(confirmMessage, {
+ title: t('withdraw.confirm_title'),
+ confirmText: t('common.confirm'),
+ cancelText: t('common.cancel'),
+ type: 'info'
+ });
+
+ if (!confirmed) {
+ btnEl.disabled = false; // 取消时恢复按钮
+ return;
+ }
+
+ // 执行确认打款
+ btnEl.disabled = true;
+ btnEl.textContent = `${t('common.processing')}...`;
+
+ // 超时保护(30秒)
+ const timeoutTimer = setTimeout(() => {
+ btnEl.disabled = false;
+ btnEl.textContent = t('withdraw.confirm_payment');
+ showToast(t('common.timeout') || '操作超时,请重试', "warning");
+ }, 30000);
+
+ try {
+ await api.completeWithdrawal(txId, paymentOrderId);
+ clearTimeout(timeoutTimer);
+ showToast(t('withdraw.payment_success'), "success");
+ // 刷新列表
+ renderWithdrawList(container, currentUser, statusFilter);
+ } catch (err) {
+ clearTimeout(timeoutTimer);
+ showToast(t('withdraw.payment_failed') + ": " + err.message, "error");
+ btnEl.disabled = false;
+ btnEl.textContent = t('withdraw.confirm_payment');
+ }
+ };
+
+ } catch (err) {
+ container.querySelector("#withdrawList").innerHTML = `
+
❌ ${t('common.load_failed')}: ${err.message}
+ `;
+ }
+}
+
+function formatTime(timestamp) {
+ if (!timestamp) return t('common.unknown');
+ const date = typeof timestamp === "number"
+ ? new Date(timestamp * 1000)
+ : new Date(timestamp);
+ if (isNaN(date.getTime())) return t('common.unknown');
+ return date.toLocaleString(getLanguage(), { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
+}
+
+function escapeHtml(str) {
+ const div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+}