diff --git a/app/assets/delete-brand.js b/app/assets/delete-brand.js index 2633354..092e1ce 100644 --- a/app/assets/delete-brand.js +++ b/app/assets/delete-brand.js @@ -1 +1 @@ -import{d as Me,o as Ae,I as Be,a as u,c as i,b as qe,B as Ue,e as r,F as R,r as Z,t as c,f,J as Ke,K as Ie,n as be,g as p,h as j,L as He,i as M,E as h,M as Je,N as Oe,O as Le,P as je,_ as Ve,p as ze,q as Ge}from"./pywebview-V_UIajJm.js";import{e as Qe}from"./brand-1nUVsWrm.js";const Xe={class:"page-shell module-page"},We={class:"main-content"},Ye={class:"left-panel"},Ze={class:"upload-zone"},es={class:"selected-files clean-placeholder split-selected-files"},ss={key:0,class:"more-line"},ts={key:1},ns={class:"run-row"},as=["disabled"],ls={class:"loading-msg"},rs={key:0,class:"queue-debug-card"},os={key:0,class:"queue-debug-line"},us={key:1,class:"queue-debug-payload"},is={class:"right-panel"},cs={class:"task-list-wrap"},ds={class:"clean-result-summary"},fs={class:"summary-card"},vs={class:"summary-card"},hs={class:"summary-card"},ps={class:"result-list-wrap"},_s={key:0,class:"empty-tasks"},gs={key:0,class:"result-subsection"},ys={class:"task-list clean-result-list"},ks={class:"left split-result-main"},ms=["title"],Is={class:"files"},bs={class:"files"},ws={key:0,class:"files"},Ss={key:1,class:"files"},Cs={key:2,class:"files"},Fs={key:3,class:"time"},Ts={key:4,class:"delete-brand-progress-block"},xs={class:"delete-brand-progress-header"},Ns={class:"delete-brand-progress-bar"},$s={class:"files delete-brand-progress"},Es={key:5,class:"files split-entry-list delete-brand-preview"},Ps={key:6,class:"files"},Ds={class:"task-right split-result-actions"},Rs=["onClick"],Ms=["onClick"],As=["onClick"],Bs={key:1,class:"result-subsection"},qs={class:"task-list clean-result-list"},Us={class:"left split-result-main"},Ks=["title"],Hs={class:"files"},Js={class:"files"},Os={key:0,class:"files"},Ls={key:1,class:"files"},js={key:2,class:"time"},Vs={key:3,class:"delete-brand-progress-block"},zs={class:"delete-brand-progress-header"},Gs={class:"delete-brand-progress-bar"},Qs={class:"files delete-brand-progress"},Xs={key:4,class:"files split-entry-list delete-brand-preview"},Ws={key:5,class:"files"},Ys={class:"task-right split-result-actions"},Zs=["onClick"],et=["onClick"],st=Me({__name:"BrandDeleteBrandTab",setup(nt){function N(){return`delete-brand:current-tasks:${typeof window<"u"&&window.localStorage.getItem("uid")||"0"}`}const C=p([]),A=p([]),$=p(!1),ee=p([]),d=p([]),B=p([]),q=p({total:0,successCount:0,failedCount:0,items:[]}),I=p(""),k=p(""),_=p({}),y=p(null),V=p(!1),F=p(!1),b=p(""),U=p(!1),z=j(()=>C.value.slice(0,10)),m=j(()=>{const e=[];return d.value.forEach(s=>{e.push(...s.items)}),e});function we(){try{const e=typeof window<"u"?window.localStorage.getItem(N()):null;if(e){if(d.value=JSON.parse(e),d.value.length>0){const s=d.value[d.value.length-1];I.value=s.queuePushResult||"",k.value=s.queuePayloadText||""}}else d.value=[]}catch{d.value=[]}}function se(e,s,t,n){const l=s.filter(v=>v.matchStatus==="MATCHED"&&v.success!==!1);if(l.length===0)return;const o={taskId:e,items:l,createdAt:Date.now(),queuePushResult:t,queuePayloadText:n},a=d.value.findIndex(v=>v.taskId===e);a>=0?d.value[a]=o:d.value.push(o),typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(d.value))}function te(e){const s=d.value.length;d.value=d.value.filter(t=>t.taskId!==e),d.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(d.value))}const K=j(()=>B.value.filter(e=>!m.value.some(s=>s.taskId===e.taskId||s.resultId&&s.resultId===e.resultId))),Se=j(()=>m.value.length>0||K.value.length>0);function ne(){typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(d.value))}function E(e){return e?(d.value.find(t=>t.taskId===e)?.items.length||0)>1:!1}function G(e){return e&&d.value.find(s=>s.taskId===e)||null}function Q(e,s){return!e||!s?!1:e.resultId!=null&&s.resultId!=null?e.resultId===s.resultId:e.fileKey&&s.fileKey?e.fileKey===s.fileKey:e.sourceFilename===s.sourceFilename}function T(e,s){const t=G(e);return t&&t.items.find(n=>Q(n,s))||null}function ae(e){if(!e)return!1;const s=G(e);if(!s)return!1;let t=!1;const n=_.value[e]?.line_progress?.info;if(E(e)&&n?.finished_files!=null&&n.finished_files>=0){const l=s.items.filter(a=>a._pushed),o=Math.max(0,Math.min(n.finished_files,l.length));for(let a=0;a0&&n.current_line>=n.total_lines){const l=m.value.find(a=>x(a)===b.value),o=l&&l.taskId===e?T(e,l):s.items.find(a=>a.sourceFilename===n.file_name);o&&!o._completed&&(o._completed=!0,t=!0)}return t&&ne(),t}function le(e){return e&&_.value[e]?.task?.status||""}function H(e){if(e.error)return e.error;if(e.matchStatus&&e.matchStatus!=="MATCHED")return e.matchMessage||"";const s=e.taskId;return s&&_.value[s]?.task?.errorMessage||""}function re(e){const s=e.taskId;if(!s)return!1;const t=_.value[s]?.line_progress,n=le(s),l=E(s),o=S(e);if(l){const a=T(s,e),v=x(e)===b.value,g=!!(t?.has_progress&&t?.info?.file_name===e.sourceFilename);return!!(v||g||a?._pushed||(n==="RUNNING"||n==="PENDING"||!n)&&a?._completed)}return o==="本文件解析完毕"||o==="已被全局判定为终态"?!1:t?.has_progress||t?.info?!0:o==="已入队"||o==="处理中"}function oe(e){return e.map(s=>s.matchStatus==="MATCHED"?{...s,_pushed:!1,_completed:!1}:{...s,matched:!1,openStoreUrl:void 0,error:s.error||void 0,_pushed:!1,_completed:!1})}function ue(e){return e.matchStatus==="MATCHED"?`已匹配 ${e.shopId||""}`.trim():e.matchMessage?e.matchMessage:e.matchStatus==="CONFLICT"?"存在多个同名店铺,请人工确认":e.matchStatus==="PENDING"||e.matchStatus==="INDEX_STALE"?"店铺索引暂未就绪":e.matchStatus?e.matchStatus:e.matched?`已匹配 ${e.shopId||""}`.trim():"待匹配"}function Ce(){ee.value=[...m.value,...K.value]}function w(){Ce(),Fe(ee.value)}function Fe(e){q.value={total:e.length,successCount:e.filter(s=>s.success).length,failedCount:e.filter(s=>!s.success).length,items:e}}function ie(e){const s=e.taskId;return s&&(_.value[s]?.fileProgress||[]).find(l=>l.fileKey&&e.fileKey?l.fileKey===e.fileKey:l.sourceFilename&&e.sourceFilename?l.sourceFilename===e.sourceFilename:!1)||null}function ce(e){const s=e.taskId;if(!s)return"";const t=_.value[s],n=t?.line_progress?.info,l=ie(e);if(l){const a=[],v=l.processedRows??0,g=l.totalRows??0;if(l.status&&a.push(`文件状态:${l.status}`),a.push(`文件进度:${v}/${g}`),n?.finished_files!=null){const D=n.file_total&&n.file_total>0?`/${n.file_total}`:"";a.push(`已完成文件:${n.finished_files}${D}`)}return n?.phase&&a.push(`阶段:${n.phase}`),a.join("|")}if(!t?.line_progress?.has_progress||!n){const a=t?.task?.status;return a?`任务状态:${a}`:""}const o=[];if(n.phase&&o.push(`阶段:${n.phase}`),n.file_index&&n.file_total&&o.push(`文件:${n.file_index}/${n.file_total}`),n.file_name&&o.push(`当前文件:${n.file_name}`),n.current_line!=null&&n.total_lines!=null&&o.push(`${E(s)?"当前文件进度":"进度"}:${n.current_line}/${n.total_lines}`),n.current_country&&o.push(`国家:${n.current_country}`),n.current_asin&&o.push(`ASIN:${n.current_asin}`),n.finished_files!=null){const a=n.file_total&&n.file_total>0?`/${n.file_total}`:"";o.push(`已完成文件:${n.finished_files}${a}`)}return o.join("|")}function J(e){const s=e.taskId;if(!s)return 0;const t=ie(e);if(t)return Math.max(0,Math.min(100,t.percent??0));const n=_.value[s]?.line_progress?.info;return n?n.current_line!=null&&n.total_lines&&n.total_lines>0?Math.max(0,Math.min(100,Math.round(n.current_line/n.total_lines*100))):n.finished_files!=null&&n.file_total&&n.file_total>0?Math.max(0,Math.min(100,Math.round(n.finished_files/n.file_total*100))):_.value[s]?.task?.status==="SUCCESS"?100:0:0}function de(e){const s=_.value[e]?.task?.status;return s==="SUCCESS"||s==="FAILED"}function P(){const e=new Set;return d.value.forEach(s=>{s.items.some(t=>t._pushed)&&e.add(s.taskId)}),Array.from(e)}function S(e){const s=e.taskId;if(!s||!G(s))return"";const n=T(s,e),l=le(s),o=_.value[s]?.line_progress,a=E(s);if(!a&&(l==="SUCCESS"||l==="FAILED"||e.taskStatus==="SUCCESS"||e.taskStatus==="FAILED"))return"已被全局判定为终态";if(n?._completed)return"本文件解析完毕";if(l==="RUNNING"&&o?.has_progress&&o?.info?.file_name===e.sourceFilename)return"处理中";if(a&&!n?._pushed&&!n?._completed)return"未入队";if(a&&l==="SUCCESS")return"本文件解析完毕";if(!a){const v=_.value[s];if(v?.items&&v.items.find(D=>Q(D,e))&&l==="SUCCESS")return"本文件解析完毕"}return n?._pushed?"已入队":"未入队"}function fe(e){if(e.downloadUrl)return!0;const s=e.taskId;return s?_.value[s]?.task?.status==="SUCCESS":!1}function Te(e){const s=e?.task?.id;if(!s||!e?.items?.length)return!1;const t=d.value.find(a=>a.taskId===s);if(!t)return!1;let n=!1;const o=t.items.map(a=>{const v=e.items?.find(D=>Q(D,a));if(!v)return a;const g={...a,...v,success:a.success??v.success,_pushed:a._pushed,_completed:a._completed};return JSON.stringify(g)!==JSON.stringify(a)&&(n=!0),g}).filter(a=>a.matchStatus==="MATCHED"&&a.success!==!1);return JSON.stringify(o)!==JSON.stringify(t.items)&&(t.items=o,n=!0),n&&ne(),n}async function X(e){const s=e||P();if(s.length)try{const t=await He(s);let n=!1;for(const l of t.items||[]){const o=l?.task?.id;typeof o=="number"&&o>0&&(_.value[o]=l,Te(l)&&(n=!0),ae(o)&&(n=!0))}n&&w()}catch{}}function xe(){return document.visibilityState==="visible"?4500:1e4}function W(e=!1){if(y.value){if(!e)return;clearTimeout(y.value),y.value=null}const s=async()=>{if(y.value=null,V.value){W();return}const t=P();if(!t.length)return;V.value=!0;let n=!1;try{await X(t),await De();for(const l of t)de(l)&&(te(l),n=!0)}finally{V.value=!1}n&&(L(),w()),P().length>0&&W()};e?s():y.value=window.setTimeout(s,xe())}function Y(e=!1){y.value&&!e||W(e)}function O(e){const s=e.taskId?_.value[e.taskId]?.task?.status:null;return s==="SUCCESS"?{className:"success",text:"已完成"}:s==="FAILED"?{className:"failed",text:"失败"}:s==="RUNNING"?{className:"running",text:"执行中"}:e.taskStatus==="SUCCESS"?{className:"success",text:"已完成"}:e.taskStatus==="FAILED"?{className:"failed",text:"失败"}:e.taskStatus==="RUNNING"?{className:"running",text:"执行中"}:e.success?{className:"success",text:"已完成"}:{className:"failed",text:"失败"}}function ve(){y.value&&(window.clearTimeout(y.value),y.value=null)}async function he(e){const s=M();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const n of e){const l=typeof n=="string"?n:n.absolutePath,o=typeof n=="string"?void 0:n.relativePath,a=await s.upload_file_to_java(l,o);if(!a?.success||!a.data)throw new Error(a?.error||a?.message||`上传失败:${l}`);t.push(a.data)}return t}async function Ne(){const e=M();if(!e?.select_brand_xlsx_files){h.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const s=await e.select_brand_xlsx_files();if(!s?.length)return;C.value=s,A.value=await he(s),h.success(`已选择 ${s.length} 个删除品牌文件`)}catch(s){h.error(s instanceof Error?s.message:"选择失败")}}async function $e(){const e=M();if(!e?.select_brand_folder){h.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await Qe(s);if(!t.success||!t.items?.length){h.warning(t.error||"该文件夹下没有 xlsx 文件");return}C.value=t.items.map(n=>n.relativePath||n.absolutePath),A.value=await he(t.items),h.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`)}catch(s){h.error(s instanceof Error?s.message:"选择失败")}}async function Ee(){if(!A.value.length){h.warning("请先选择待处理文件");return}try{$.value=!0;const e=await Je({files:A.value.map(l=>({fileKey:l.fileKey,originalFilename:l.originalFilename,relativePath:l.relativePath}))}),s=oe(e.items||[]),t=s.some(l=>l.matchStatus==="MATCHED"),n=s.some(l=>l.matchStatus&&l.matchStatus!=="MATCHED");I.value="解析完成,等待手动点击打开紫鸟进行单任务处理",k.value="",s.length&&s[0].taskId&&se(s[0].taskId,s,I.value,k.value),w(),n?h.warning("部分文件尚未命中可用店铺索引,已保留状态信息,请等待后台刷新后重试。"):t?h.success("删除品牌解析完成,请手动推送"):h.warning("当前没有可推送的已匹配文件,请先等待店铺索引刷新。"),await L()}catch(e){h.error(e instanceof Error?e.message:"执行失败")}finally{$.value=!1}}async function pe(e){const s=e.taskId;if(!s){h.warning("未找到任务ID");return}const t=M(),n=Oe(s),l=e.outputFilename||e.sourceFilename||`delete-brand_${s}.xlsx`;if(t?.save_file_from_url){const o=await t.save_file_from_url(n,l);o.success?h.success(`已保存:${o.path||l}`):o.error&&o.error!=="用户取消"&&h.error(o.error);return}window.open(n,"_blank")}async function L(){try{const e=await Le();B.value=oe(e.items||[]),w(),await X(),Y(!0)}catch(e){console.error("loadHistory error:",e)}}function _e(e){d.value.forEach(t=>{t.items=t.items.filter(n=>n.resultId!==e)});const s=d.value.length;d.value=d.value.filter(t=>t.items.length>0),d.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(d.value)),B.value=B.value.filter(t=>t.resultId!==e),w(),P().length===0&&ve()}async function ge(e){try{await je(e),_e(e),await L(),h.success("已删除")}catch(s){const t=s instanceof Error?s.message:"删除失败";if(typeof t=="string"&&t.includes("记录不存在")){_e(e),h.success("已删除");return}h.error(t)}}function ye(e){const s=e.slice(0,5).map(n=>`${n.country}:${n.asin}${n.status?`(${n.status})`:""}`);return e.length-s.length>0?`${s.join("、")} 等 ${e.length} 条`:s.join("、")}function at(e,s){}function ke(){for(const e of d.value)for(const s of e.items){const t=S(s);if(t==="已入队"||t==="处理中")return!0}return!1}function x(e){return e.resultId!=null?`result:${e.resultId}`:`task:${e.taskId||0}:${e.sourceFilename||""}`}function Pe(){return m.value.find(e=>!e.openStoreUrl||x(e)===b.value||T(e.taskId,e)?._completed?!1:S(e)==="未入队")}async function me(e,s){if(s?.auto&&ke())return e.taskId,e.sourceFilename,!1;const t=M(),n=e.taskId;if(!n)return!1;const l=d.value.find(g=>g.taskId===n);if(!l)return!1;if(!t?.enqueue_json)return h.error("当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)"),!1;const o={type:"delete-brand-run",ts:Date.now(),data:{taskId:n,items:[e]}};k.value=JSON.stringify(o,null,2);const a=await t.enqueue_json(o);let v="";if(a?.success){v=`已推送文件 ${e.sourceFilename},当前队列长度:${a.queue_size??"-"}`;const g=T(n,e);g&&(g._pushed=!0,g._completed=!1),b.value=x(e),se(n,l.items,v,k.value),Y(!0)}else v=`推送到 Python 队列失败:${a?.error||"未知错误"}`;return I.value=v,!!a?.success}async function De(){if(!F.value||U.value){F.value,U.value;return}if(ke())return;const e=b.value;if(e){const t=m.value.find(n=>x(n)===e);if(t){const n=t.taskId,l=E(n);l&&(ae(n),S(t),void 0);const o=S(t);if(o==="已入队"||o==="处理中"){t.sourceFilename;return}if(l){const a=T(n,t);if(a&&!a._completed){t.sourceFilename,a._pushed,a._completed;return}}}}const s=Pe();if(!s){b.value="",F.value=!1;return}U.value=!0;try{s.taskId,s.sourceFilename,x(s),await me(s,{auto:!0})||(s.taskId,s.sourceFilename,F.value=!1)}finally{U.value=!1}}async function Re(e){F.value=!0,await me(e)||(F.value=!1,b.value="")}return Ae(()=>{we();const e=P();e.length>0?X(e).then(()=>{let s=!1;for(const t of e)de(t)&&(te(t),s=!0);s&&w(),Y(!0)}):w(),L().catch(()=>{})}),Be(()=>{ve()}),(e,s)=>(u(),i("div",Xe,[qe(Ue,{active:"delete-brand"}),r("div",We,[r("aside",Ye,[s[2]||(s[2]=r("div",{class:"section-title"},"选择文件",-1)),r("div",Ze,[s[0]||(s[0]=r("div",{class:"hint"},"上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名作为店铺名。",-1)),r("div",{class:"btns"},[r("button",{type:"button",class:"opt-btn",onClick:Ne},"选择 Excel 文件"),r("button",{type:"button",class:"opt-btn",onClick:$e},"选择文件夹")]),r("div",es,[C.value.length?(u(),i(R,{key:0},[(u(!0),i(R,null,Z(z.value,t=>(u(),i("span",{key:t},c(t),1))),128)),C.value.length>z.value.length?(u(),i("span",ss," 还有 "+c(C.value.length-z.value.length)+" 个文件未展开显示 ",1)):f("",!0)],64)):(u(),i("span",ts,"暂未选择删除品牌文件"))])]),s[3]||(s[3]=Ke('
执行说明
',2)),r("div",ns,[r("button",{type:"button",class:"btn-run",disabled:$.value,onClick:Ee},c($.value?"解析中...":"开始解析"),9,as),r("span",ls,c($.value?"正在读取删除品牌数据,请稍候…":"解析后会在右侧展示按国家分组的去重结果"),1)]),I.value||k.value?(u(),i("div",rs,[s[1]||(s[1]=r("div",{class:"section-title queue-debug-title"},"Python 队列推送结果",-1)),I.value?(u(),i("div",os,c(I.value),1)):f("",!0),k.value?(u(),i("pre",us,c(k.value),1)):f("",!0)])):f("",!0)]),r("section",is,[s[12]||(s[12]=r("div",{class:"panel-header"},"删除品牌结果",-1)),r("div",cs,[r("div",ds,[r("div",fs,[s[4]||(s[4]=r("span",{class:"summary-label"},"已处理文件",-1)),r("strong",null,c(q.value.total),1)]),r("div",vs,[s[5]||(s[5]=r("span",{class:"summary-label"},"成功结果",-1)),r("strong",null,c(q.value.successCount),1)]),r("div",hs,[s[6]||(s[6]=r("span",{class:"summary-label"},"失败文件",-1)),r("strong",null,c(q.value.failedCount),1)])]),r("div",ps,[s[11]||(s[11]=r("div",{class:"result-list-header"},[r("span",null,"删除品牌结果列表")],-1)),Se.value?(u(),i(R,{key:1},[m.value.length?(u(),i("div",gs,[s[8]||(s[8]=r("div",{class:"result-subsection-title"},"当前任务",-1)),r("ul",ys,[(u(!0),i(R,null,Z(m.value,t=>(u(),i("li",{key:`current-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[r("div",ks,[r("span",{class:"id",title:t.sourceFilename},c(t.sourceFilename||"-"),9,ms),r("div",Is,"店铺名:"+c(t.shopName||"-"),1),r("div",bs,"匹配结果:"+c(ue(t)),1),t.platform?(u(),i("div",ws,"平台:"+c(t.platform),1)):f("",!0),S(t)?(u(),i("div",Ss,"队列状态:"+c(S(t)),1)):f("",!0),t.countryCount!==void 0&&t.matched?(u(),i("div",Cs,"国家数:"+c(t.countryCount),1)):f("",!0),t.totalRows!==void 0&&t.matched?(u(),i("div",Fs,"去重后 "+c(t.totalRows)+" 条 ",1)):f("",!0),re(t)?(u(),i("div",Ts,[r("div",xs,[s[7]||(s[7]=r("span",null,"任务进度",-1)),r("span",null,c(J(t))+"%",1)]),r("div",Ns,[r("div",{class:"delete-brand-progress-bar-fill",style:Ie({width:`${J(t)}%`})},null,4)]),r("div",$s,c(ce(t)),1)])):f("",!0),t.previewRows?.length?(u(),i("div",Es,c(ye(t.previewRows)),1)):f("",!0),H(t)?(u(),i("div",Ps,"错误信息:"+c(H(t)),1)):f("",!0)]),r("div",Ds,[r("span",{class:be(["status",O(t).className])},c(O(t).text),3),t.openStoreUrl?(u(),i("button",{key:0,type:"button",class:"download",onClick:n=>Re(t)}," 打开紫鸟 ",8,Rs)):f("",!0),fe(t)?(u(),i("button",{key:1,type:"button",class:"download",onClick:n=>pe(t)}," 下载 ",8,Ms)):f("",!0),t.resultId?(u(),i("button",{key:2,type:"button",class:"btn-delete",onClick:n=>ge(t.resultId)}," 删除 ",8,As)):f("",!0)])]))),128))])])):f("",!0),K.value.length?(u(),i("div",Bs,[s[10]||(s[10]=r("div",{class:"result-subsection-title"},"历史记录",-1)),r("ul",qs,[(u(!0),i(R,null,Z(K.value,t=>(u(),i("li",{key:`history-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[r("div",Us,[r("span",{class:"id",title:t.sourceFilename},c(t.sourceFilename||"-"),9,Ks),r("div",Hs,"店铺名:"+c(t.shopName||"-"),1),r("div",Js,"匹配结果:"+c(ue(t)),1),t.platform?(u(),i("div",Os,"平台:"+c(t.platform),1)):f("",!0),t.countryCount!==void 0&&t.matched?(u(),i("div",Ls,"国家数:"+c(t.countryCount),1)):f("",!0),t.totalRows!==void 0&&t.matched?(u(),i("div",js,"去重后 "+c(t.totalRows)+" 条 ",1)):f("",!0),re(t)?(u(),i("div",Vs,[r("div",zs,[s[9]||(s[9]=r("span",null,"任务进度",-1)),r("span",null,c(J(t))+"%",1)]),r("div",Gs,[r("div",{class:"delete-brand-progress-bar-fill",style:Ie({width:`${J(t)}%`})},null,4)]),r("div",Qs,c(ce(t)),1)])):f("",!0),t.previewRows?.length?(u(),i("div",Xs,c(ye(t.previewRows)),1)):f("",!0),H(t)?(u(),i("div",Ws,"错误信息:"+c(H(t)),1)):f("",!0)]),r("div",Ys,[r("span",{class:be(["status",O(t).className])},c(O(t).text),3),fe(t)?(u(),i("button",{key:0,type:"button",class:"download",onClick:n=>pe(t)}," 下载 ",8,Zs)):f("",!0),t.resultId?(u(),i("button",{key:1,type:"button",class:"btn-delete",onClick:n=>ge(t.resultId)}," 删除 ",8,et)):f("",!0)])]))),128))])])):f("",!0)],64)):(u(),i("div",_s," 暂无删除品牌结果,完成解析后会在这里展示按国家分组的去重结果 "))])])])])]))}}),tt=Ve(st,[["__scopeId","data-v-7f166b99"]]);ze(tt).use(Ge).mount("#app"); +import{d as Oe,o as He,I as Je,a as u,c,b as Ge,B as je,e as o,F as O,r as oe,t as f,f as p,J as Ve,K as Fe,n as Ne,g as m,h as Y,L as ze,i as H,E as h,M as Qe,N as Xe,O as We,P as Ye,_ as Ze,p as es,q as ss}from"./pywebview-V_UIajJm.js";import{e as ts}from"./brand-1nUVsWrm.js";const ns={class:"page-shell module-page"},as={class:"main-content"},ls={class:"left-panel"},os={class:"upload-zone"},rs={class:"selected-files clean-placeholder split-selected-files"},is={key:0,class:"more-line"},us={key:1},cs={class:"run-row"},ds=["disabled"],fs={class:"loading-msg"},vs={key:0,class:"queue-debug-card"},ps={key:0,class:"queue-debug-line"},hs={key:1,class:"queue-debug-payload"},_s={class:"right-panel"},gs={class:"task-list-wrap"},ys={class:"clean-result-summary"},ks={class:"summary-card"},ms={class:"summary-card"},ws={class:"summary-card"},Is={class:"result-list-wrap"},Ss={key:0,class:"empty-tasks"},bs={key:0,class:"result-subsection"},Cs={class:"task-list clean-result-list"},Ts={class:"left split-result-main"},Fs=["title"],Ns={class:"files"},Es={class:"files"},Rs={key:0,class:"files"},xs={key:1,class:"files"},$s={key:2,class:"files"},Ps={key:3,class:"time"},As={key:4,class:"delete-brand-progress-block"},Ds={class:"delete-brand-progress-header"},Ms={class:"delete-brand-progress-bar"},Bs={class:"files delete-brand-progress"},Us={key:5,class:"files split-entry-list delete-brand-preview"},qs={key:6,class:"files"},Ls={class:"task-right split-result-actions"},Ks=["onClick"],Os=["onClick"],Hs=["onClick"],Js={key:1,class:"result-subsection"},Gs={class:"task-list clean-result-list"},js={class:"left split-result-main"},Vs=["title"],zs={class:"files"},Qs={class:"files"},Xs={key:0,class:"files"},Ws={key:1,class:"files"},Ys={key:2,class:"time"},Zs={key:3,class:"delete-brand-progress-block"},et={class:"delete-brand-progress-header"},st={class:"delete-brand-progress-bar"},tt={class:"files delete-brand-progress"},nt={key:4,class:"files split-entry-list delete-brand-preview"},at={key:5,class:"files"},lt={class:"task-right split-result-actions"},ot=["onClick"],rt=["onClick"],it=Oe({__name:"BrandDeleteBrandTab",setup(ct){function M(){return`delete-brand:current-tasks:${typeof window<"u"&&window.localStorage.getItem("uid")||"0"}`}const $=m([]),J=m([]),B=m(!1),re=m([]),v=m([]),G=m([]),j=m({total:0,successCount:0,failedCount:0,items:[]}),T=m(""),C=m(""),g=m({}),S=m(null),Z=m(!1),P=m(!1),A=m(""),V=m(!1),ee=Y(()=>$.value.slice(0,10)),F=Y(()=>{const e=[];return v.value.forEach(s=>{e.push(...s.items)}),e});function Ee(){try{const e=typeof window<"u"?window.localStorage.getItem(M()):null;if(e){if(v.value=JSON.parse(e),v.value.length>0){const s=v.value[v.value.length-1];T.value=s.queuePushResult||"",C.value=s.queuePayloadText||""}}else v.value=[]}catch{v.value=[]}}function ie(e,s,t,n){if(!s?.length)return;const a=v.value.findIndex(d=>d.taskId===e),r=a>=0?v.value[a]:null,l=s.map(d=>{const _=r?.items.find(Ke=>D(Ke,d)),k=Array.isArray(d.countries)?d.countries:void 0,y=Array.isArray(_?.countries)?_?.countries:void 0,w=k&&k.length>0?k:y&&y.length>0?y:k??y,I=Array.isArray(d.previewRows)?d.previewRows:void 0,b=Array.isArray(_?.previewRows)?_?.previewRows:void 0,le=I&&I.length>0?I:b&&b.length>0?b:I??b;return{...d,countries:w,previewRows:le,_pushed:_?._pushed??d._pushed??!1,_completed:_?._completed??d._completed??!1}}),i={taskId:e,items:l,createdAt:r?.createdAt||Date.now(),queuePushResult:t??r?.queuePushResult,queuePayloadText:n??r?.queuePayloadText};a>=0?v.value[a]=i:v.value.push(i),typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}function ue(e){const s=v.value.length;v.value=v.value.filter(t=>t.taskId!==e),v.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}const z=Y(()=>G.value.filter(e=>!F.value.some(s=>D(s,e)))),Re=Y(()=>F.value.length>0||z.value.length>0);function ce(){typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}function N(e){return e?(v.value.find(t=>t.taskId===e)?.items.length||0)>1:!1}function U(e){return e&&v.value.find(s=>s.taskId===e)||null}function D(e,s){return!e||!s?!1:e.resultId!=null&&s.resultId!=null?e.resultId===s.resultId:e.fileKey&&s.fileKey?e.fileKey===s.fileKey:e.sourceFilename===s.sourceFilename}function E(e,s){const t=U(e);return t&&t.items.find(n=>D(n,s))||null}function de(e){if(!e)return!1;const s=U(e);if(!s)return!1;let t=!1;const n=g.value[e]?.line_progress?.info,a=N(e);if(n?.file_name&&n.current_line!=null&&n.total_lines!=null&&n.total_lines>0&&n.current_line>=n.total_lines){const r=s.items.find(l=>l.sourceFilename===n.file_name);r&&!r._completed&&(r._completed=!0,t=!0)}if(!a){const r=g.value[e]?.task?.status;if((r==="SUCCESS"||r==="FAILED")&&s.items.length===1){const l=s.items[0];l&&!l._completed&&(l._completed=!0,t=!0)}}return t&&ce(),t}function fe(e){return e&&g.value[e]?.task?.status||""}function Q(e){if(e.error)return e.error;if(e.matchStatus&&e.matchStatus!=="MATCHED")return e.matchMessage||"";const s=e.taskId;return s&&g.value[s]?.task?.errorMessage||""}function ve(e){const s=e.taskId;if(!s)return!1;const t=g.value[s]?.line_progress,n=fe(s),a=N(s),r=x(e);if(a){const l=E(s,e),i=K(e)===A.value,d=!!(t?.has_progress&&t?.info?.file_name===e.sourceFilename);return!!(i||d||l?._pushed||(n==="RUNNING"||n==="PENDING"||!n)&&l?._completed)}return r==="本文件解析完毕"||r==="已被全局判定为终态"?!1:t?.has_progress||t?.info?!0:r==="已入队"||r==="处理中"}function pe(e){return e.map(s=>s.matchStatus==="MATCHED"?{...s,_pushed:!1,_completed:!1}:{...s,matched:!1,openStoreUrl:void 0,error:s.error||void 0,_pushed:!1,_completed:!1})}function he(e){return e.matchStatus==="MATCHED"?`已匹配 ${e.shopId||""}`.trim():e.matchMessage?e.matchMessage:e.matchStatus==="CONFLICT"?"存在多个同名店铺,请人工确认":e.matchStatus==="PENDING"||e.matchStatus==="INDEX_STALE"?"店铺索引暂未就绪":e.matchStatus?e.matchStatus:e.matched?`已匹配 ${e.shopId||""}`.trim():"待匹配"}function xe(){re.value=[...F.value,...z.value]}function R(){xe(),$e(re.value)}function $e(e){j.value={total:e.length,successCount:e.filter(s=>s.success).length,failedCount:e.filter(s=>!s.success).length,items:e}}function se(e){const s=e.taskId;return s&&(g.value[s]?.fileProgress||[]).find(a=>a.fileKey&&e.fileKey?a.fileKey===e.fileKey:a.sourceFilename&&e.sourceFilename?a.sourceFilename===e.sourceFilename:!1)||null}function _e(e){const s=e.taskId;if(!s)return"";const t=g.value[s],n=t?.line_progress?.info,a=se(e);if(N(s)){const i=E(s,e),d=t?.task?.status,_=!!(n?.file_name&&n.file_name===e.sourceFilename);let k=i?._completed?"COMPLETED":(i?._pushed,"PENDING"),y=0,w=Math.max(0,e.totalRows??a?.totalRows??0);d==="SUCCESS"||d==="FAILED"&&i?._completed?(k="COMPLETED",y=w):_?(k="RUNNING",y=Math.max(0,Math.min(w||Number.MAX_SAFE_INTEGER,n?.current_line??0)),!w&&n?.total_lines&&n.total_lines>0&&(w=n.total_lines)):i?._completed?(k="COMPLETED",y=w):i?._pushed&&(k="PENDING",y=0);const I=[];if(I.push(`文件状态:${k}`),I.push(`文件进度:${y}/${w}`),n?.finished_files!=null){const b=n.file_total&&n.file_total>0?`/${n.file_total}`:"";I.push(`已完成文件:${n.finished_files}${b}`)}return n?.phase&&I.push(`阶段:${n.phase}`),I.join("|")}if(a){const i=[],d=a.processedRows??0,_=a.totalRows??0;if(a.status&&i.push(`文件状态:${a.status}`),i.push(`文件进度:${d}/${_}`),n?.finished_files!=null){const k=n.file_total&&n.file_total>0?`/${n.file_total}`:"";i.push(`已完成文件:${n.finished_files}${k}`)}return n?.phase&&i.push(`阶段:${n.phase}`),i.join("|")}if(!t?.line_progress?.has_progress||!n){const i=t?.task?.status;return i?`任务状态:${i}`:""}const l=[];if(n.phase&&l.push(`阶段:${n.phase}`),n.file_index&&n.file_total&&l.push(`文件:${n.file_index}/${n.file_total}`),n.file_name&&l.push(`当前文件:${n.file_name}`),n.current_line!=null&&n.total_lines!=null&&l.push(`${N(s)?"当前文件进度":"进度"}:${n.current_line}/${n.total_lines}`),n.current_country&&l.push(`国家:${n.current_country}`),n.current_asin&&l.push(`ASIN:${n.current_asin}`),n.finished_files!=null){const i=n.file_total&&n.file_total>0?`/${n.file_total}`:"";l.push(`已完成文件:${n.finished_files}${i}`)}return l.join("|")}function X(e){const s=e.taskId;if(!s)return 0;const t=g.value[s]?.line_progress?.info;if(N(s)){const l=E(s,e);if(g.value[s]?.task?.status==="SUCCESS"||l?._completed)return 100;const d=Math.max(0,e.totalRows??se(e)?.totalRows??0);if(!!(t?.file_name&&t.file_name===e.sourceFilename)){if(t?.current_line!=null&&t?.total_lines&&t.total_lines>0)return Math.max(0,Math.min(100,Math.round(t.current_line/t.total_lines*100)));if(t?.current_line!=null&&d>0)return Math.max(0,Math.min(100,Math.round(t.current_line/d*100)))}return 0}const a=se(e);return a?Math.max(0,Math.min(100,a.percent??0)):t?t.current_line!=null&&t.total_lines&&t.total_lines>0?Math.max(0,Math.min(100,Math.round(t.current_line/t.total_lines*100))):t.finished_files!=null&&t.file_total&&t.file_total>0?Math.max(0,Math.min(100,Math.round(t.finished_files/t.file_total*100))):g.value[s]?.task?.status==="SUCCESS"?100:0:0}function ge(e){const s=g.value[e]?.task?.status;return s==="SUCCESS"||s==="FAILED"}function q(){const e=new Set;return v.value.forEach(s=>{s.items.some(t=>t._pushed)&&e.add(s.taskId)}),Array.from(e)}function x(e){const s=e.taskId;if(!s||!U(s))return"";const n=E(s,e),a=fe(s),r=g.value[s]?.line_progress,l=N(s);if(!l&&(a==="SUCCESS"||a==="FAILED"||e.taskStatus==="SUCCESS"||e.taskStatus==="FAILED"))return"已被全局判定为终态";if(n?._completed)return"本文件解析完毕";if(a==="RUNNING"&&r?.has_progress&&r?.info?.file_name===e.sourceFilename)return"处理中";if(l&&!n?._pushed&&!n?._completed)return"未入队";if(l&&a==="SUCCESS")return"本文件解析完毕";if(!l){const i=g.value[s];if(i?.items&&i.items.find(_=>D(_,e))&&a==="SUCCESS")return"本文件解析完毕"}return n?._pushed?"已入队":"未入队"}function ye(e){if(e.downloadUrl)return!0;const s=e.taskId;return s?g.value[s]?.task?.status==="SUCCESS":!1}function Pe(e){const s=e?.task?.id;if(!s)return!1;const t=v.value.find(l=>l.taskId===s);if(!t)return!1;const n=e?.items||[];let a=!1;const r=t.items.map(l=>{const i=n.find(le=>D(le,l));if(!i)return l;const d=Array.isArray(i.countries)?i.countries:void 0,_=Array.isArray(l.countries)?l.countries:void 0,k=d&&d.length>0?d:_&&_.length>0?_:d??_,y=Array.isArray(i.previewRows)?i.previewRows:void 0,w=Array.isArray(l.previewRows)?l.previewRows:void 0,I=y&&y.length>0?y:w&&w.length>0?w:y??w,b={...l,...i,countries:k,previewRows:I,_pushed:l._pushed,_completed:l._completed};return JSON.stringify(b)!==JSON.stringify(l)&&(a=!0),b});for(const l of n)r.some(i=>D(i,l))||(r.push({...l,_pushed:!1,_completed:!1}),a=!0);return JSON.stringify(r)!==JSON.stringify(t.items)&&(t.items=r,a=!0),a&&ce(),a}async function te(e){const s=e||q();if(s.length)try{const t=await ze(s);let n=!1;for(const a of t.items||[]){const r=a?.task?.id;typeof r=="number"&&r>0&&(g.value[r]=a,Pe(a)&&(n=!0),de(r)&&(n=!0))}n&&R()}catch{}}function Ae(){return document.visibilityState==="visible"?4500:1e4}function ne(e=!1){if(S.value){if(!e)return;clearTimeout(S.value),S.value=null}const s=async()=>{if(S.value=null,Z.value){ne();return}const t=q();if(!t.length)return;Z.value=!0;let n=!1,a=!1;try{await te(t),await qe();for(const r of t)ge(r)&&(U(r)?.items?.every(d=>d._completed)??!0)&&(ue(r),n=!0,a=!0)}finally{Z.value=!1}a?await L():n&&R(),q().length>0&&ne()};e?s():S.value=window.setTimeout(s,Ae())}function ae(e=!1){S.value&&!e||ne(e)}function W(e){const s=e.taskId?g.value[e.taskId]?.task?.status:null;return s==="SUCCESS"?{className:"success",text:"已完成"}:s==="FAILED"?{className:"failed",text:"失败"}:s==="RUNNING"?{className:"running",text:"执行中"}:e.taskStatus==="SUCCESS"?{className:"success",text:"已完成"}:e.taskStatus==="FAILED"?{className:"failed",text:"失败"}:e.taskStatus==="RUNNING"?{className:"running",text:"执行中"}:e.success?{className:"success",text:"已完成"}:{className:"failed",text:"失败"}}function ke(){S.value&&(window.clearTimeout(S.value),S.value=null)}async function me(e){const s=H();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const n of e){const a=typeof n=="string"?n:n.absolutePath,r=typeof n=="string"?void 0:n.relativePath,l=await s.upload_file_to_java(a,r);if(!l?.success||!l.data)throw new Error(l?.error||l?.message||`上传失败:${a}`);t.push(l.data)}return t}async function De(){const e=H();if(!e?.select_brand_xlsx_files){h.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const s=await e.select_brand_xlsx_files();if(!s?.length)return;$.value=s,J.value=await me(s),h.success(`已选择 ${s.length} 个删除品牌文件`)}catch(s){h.error(s instanceof Error?s.message:"选择失败")}}async function Me(){const e=H();if(!e?.select_brand_folder){h.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await ts(s);if(!t.success||!t.items?.length){h.warning(t.error||"该文件夹下没有 xlsx 文件");return}$.value=t.items.map(n=>n.relativePath||n.absolutePath),J.value=await me(t.items),h.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`)}catch(s){h.error(s instanceof Error?s.message:"选择失败")}}async function Be(){if(!J.value.length){h.warning("请先选择待处理文件");return}try{B.value=!0;const e=await Qe({files:J.value.map(a=>({fileKey:a.fileKey,originalFilename:a.originalFilename,relativePath:a.relativePath}))}),s=pe(e.items||[]),t=s.some(a=>a.matchStatus==="MATCHED"),n=s.some(a=>a.matchStatus&&a.matchStatus!=="MATCHED");T.value="解析完成,等待手动点击打开紫鸟进行单任务处理",C.value="",s.length&&s[0].taskId&&ie(s[0].taskId,s,T.value,C.value),R(),n?h.warning("部分文件尚未命中可用店铺索引,已保留状态信息,请等待后台刷新后重试。"):t?h.success("删除品牌解析完成,请手动推送"):h.warning("当前没有可推送的已匹配文件,请先等待店铺索引刷新。"),await L()}catch(e){h.error(e instanceof Error?e.message:"执行失败")}finally{B.value=!1}}async function we(e){const s=e.taskId;if(!s){h.warning("未找到任务ID");return}const t=H(),n=Xe(s),a=e.outputFilename||e.sourceFilename||`delete-brand_${s}.xlsx`;if(t?.save_file_from_url){const r=await t.save_file_from_url(n,a);r.success?h.success(`已保存:${r.path||a}`):r.error&&r.error!=="用户取消"&&h.error(r.error);return}window.open(n,"_blank")}async function L(){try{const e=await We();G.value=pe(e.items||[]),R(),await te(),ae(!0)}catch(e){console.error("loadHistory error:",e)}}function Ie(e){v.value.forEach(t=>{t.items=t.items.filter(n=>n.resultId!==e)});const s=v.value.length;v.value=v.value.filter(t=>t.items.length>0),v.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value)),G.value=G.value.filter(t=>t.resultId!==e),R(),q().length===0&&ke()}async function Se(e){try{await Ye(e),Ie(e),await L(),h.success("已删除")}catch(s){const t=s instanceof Error?s.message:"删除失败";if(typeof t=="string"&&t.includes("记录不存在")){Ie(e),h.success("已删除");return}h.error(t)}}function be(e){const s=e.slice(0,5).map(n=>`${n.country}:${n.asin}${n.status?`(${n.status})`:""}`);return e.length-s.length>0?`${s.join("、")} 等 ${e.length} 条`:s.join("、")}function dt(e,s){}function Ce(){for(const e of v.value)for(const s of e.items){const t=x(s);if(t==="已入队"||t==="处理中")return!0}return!1}function K(e){return e.resultId!=null?`result:${e.resultId}`:`task:${e.taskId||0}:${e.sourceFilename||""}`}function Ue(){return F.value.find(e=>!e.openStoreUrl||K(e)===A.value||E(e.taskId,e)?._completed?!1:x(e)==="未入队")}async function Te(e,s){if(s?.auto&&Ce())return e.taskId,e.sourceFilename,!1;const t=H(),n=e.taskId;if(!n)return!1;const a=v.value.find(d=>d.taskId===n);if(!a)return!1;if(!t?.enqueue_json)return h.error("当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)"),!1;const r={type:"delete-brand-run",ts:Date.now(),data:{taskId:n,items:[e]}};C.value=JSON.stringify(r,null,2);const l=await t.enqueue_json(r);let i="";if(l?.success){i=`已推送文件 ${e.sourceFilename},当前队列长度:${l.queue_size??"-"}`;const d=E(n,e);d&&(d._pushed=!0,d._completed=!1),A.value=K(e),ie(n,a.items,i,C.value),ae(!0)}else i=`推送到 Python 队列失败:${l?.error||"未知错误"}`;return T.value=i,!!l?.success}async function qe(){if(!P.value||V.value){P.value,V.value;return}if(Ce())return;const e=A.value;if(e){const t=F.value.find(n=>K(n)===e);if(t){const n=t.taskId,a=N(n);a&&(de(n),x(t),void 0);const r=x(t);if(r==="已入队"||r==="处理中"){t.sourceFilename;return}if(a){const l=E(n,t);if(l&&!l._completed){t.sourceFilename,l._pushed,l._completed;return}}}}const s=Ue();if(!s){A.value="",P.value=!1;return}V.value=!0;try{s.taskId,s.sourceFilename,K(s),await Te(s,{auto:!0})||(s.taskId,s.sourceFilename,P.value=!1)}finally{V.value=!1}}async function Le(e){P.value=!0,await Te(e)||(P.value=!1,A.value="")}return He(()=>{Ee();const e=q();e.length>0?te(e).then(async()=>{let s=!1,t=!1;for(const n of e)ge(n)&&(U(n)?.items?.every(l=>l._completed)??!0)&&(ue(n),s=!0,t=!0);t?await L():s&&R(),ae(!0)}):R(),L().catch(()=>{})}),Je(()=>{ke()}),(e,s)=>(u(),c("div",ns,[Ge(je,{active:"delete-brand"}),o("div",as,[o("aside",ls,[s[2]||(s[2]=o("div",{class:"section-title"},"选择文件",-1)),o("div",os,[s[0]||(s[0]=o("div",{class:"hint"},"上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名作为店铺名。",-1)),o("div",{class:"btns"},[o("button",{type:"button",class:"opt-btn",onClick:De},"选择 Excel 文件"),o("button",{type:"button",class:"opt-btn",onClick:Me},"选择文件夹")]),o("div",rs,[$.value.length?(u(),c(O,{key:0},[(u(!0),c(O,null,oe(ee.value,t=>(u(),c("span",{key:t},f(t),1))),128)),$.value.length>ee.value.length?(u(),c("span",is," 还有 "+f($.value.length-ee.value.length)+" 个文件未展开显示 ",1)):p("",!0)],64)):(u(),c("span",us,"暂未选择删除品牌文件"))])]),s[3]||(s[3]=Ve('
执行说明
',2)),o("div",cs,[o("button",{type:"button",class:"btn-run",disabled:B.value,onClick:Be},f(B.value?"解析中...":"开始解析"),9,ds),o("span",fs,f(B.value?"正在读取删除品牌数据,请稍候…":"解析后会在右侧展示按国家分组的去重结果"),1)]),T.value||C.value?(u(),c("div",vs,[s[1]||(s[1]=o("div",{class:"section-title queue-debug-title"},"Python 队列推送结果",-1)),T.value?(u(),c("div",ps,f(T.value),1)):p("",!0),C.value?(u(),c("pre",hs,f(C.value),1)):p("",!0)])):p("",!0)]),o("section",_s,[s[12]||(s[12]=o("div",{class:"panel-header"},"删除品牌结果",-1)),o("div",gs,[o("div",ys,[o("div",ks,[s[4]||(s[4]=o("span",{class:"summary-label"},"已处理文件",-1)),o("strong",null,f(j.value.total),1)]),o("div",ms,[s[5]||(s[5]=o("span",{class:"summary-label"},"成功结果",-1)),o("strong",null,f(j.value.successCount),1)]),o("div",ws,[s[6]||(s[6]=o("span",{class:"summary-label"},"失败文件",-1)),o("strong",null,f(j.value.failedCount),1)])]),o("div",Is,[s[11]||(s[11]=o("div",{class:"result-list-header"},[o("span",null,"删除品牌结果列表")],-1)),Re.value?(u(),c(O,{key:1},[F.value.length?(u(),c("div",bs,[s[8]||(s[8]=o("div",{class:"result-subsection-title"},"当前任务",-1)),o("ul",Cs,[(u(!0),c(O,null,oe(F.value,t=>(u(),c("li",{key:`current-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[o("div",Ts,[o("span",{class:"id",title:t.sourceFilename},f(t.sourceFilename||"-"),9,Fs),o("div",Ns,"店铺名:"+f(t.shopName||"-"),1),o("div",Es,"匹配结果:"+f(he(t)),1),t.platform?(u(),c("div",Rs,"平台:"+f(t.platform),1)):p("",!0),x(t)?(u(),c("div",xs,"队列状态:"+f(x(t)),1)):p("",!0),t.countryCount!==void 0&&t.matched?(u(),c("div",$s,"国家数:"+f(t.countryCount),1)):p("",!0),t.totalRows!==void 0&&t.matched?(u(),c("div",Ps,"去重后 "+f(t.totalRows)+" 条 ",1)):p("",!0),ve(t)?(u(),c("div",As,[o("div",Ds,[s[7]||(s[7]=o("span",null,"任务进度",-1)),o("span",null,f(X(t))+"%",1)]),o("div",Ms,[o("div",{class:"delete-brand-progress-bar-fill",style:Fe({width:`${X(t)}%`})},null,4)]),o("div",Bs,f(_e(t)),1)])):p("",!0),t.previewRows?.length?(u(),c("div",Us,f(be(t.previewRows)),1)):p("",!0),Q(t)?(u(),c("div",qs,"错误信息:"+f(Q(t)),1)):p("",!0)]),o("div",Ls,[o("span",{class:Ne(["status",W(t).className])},f(W(t).text),3),t.openStoreUrl?(u(),c("button",{key:0,type:"button",class:"download",onClick:n=>Le(t)}," 打开紫鸟 ",8,Ks)):p("",!0),ye(t)?(u(),c("button",{key:1,type:"button",class:"download",onClick:n=>we(t)}," 下载 ",8,Os)):p("",!0),t.resultId?(u(),c("button",{key:2,type:"button",class:"btn-delete",onClick:n=>Se(t.resultId)}," 删除 ",8,Hs)):p("",!0)])]))),128))])])):p("",!0),z.value.length?(u(),c("div",Js,[s[10]||(s[10]=o("div",{class:"result-subsection-title"},"历史记录",-1)),o("ul",Gs,[(u(!0),c(O,null,oe(z.value,t=>(u(),c("li",{key:`history-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[o("div",js,[o("span",{class:"id",title:t.sourceFilename},f(t.sourceFilename||"-"),9,Vs),o("div",zs,"店铺名:"+f(t.shopName||"-"),1),o("div",Qs,"匹配结果:"+f(he(t)),1),t.platform?(u(),c("div",Xs,"平台:"+f(t.platform),1)):p("",!0),t.countryCount!==void 0&&t.matched?(u(),c("div",Ws,"国家数:"+f(t.countryCount),1)):p("",!0),t.totalRows!==void 0&&t.matched?(u(),c("div",Ys,"去重后 "+f(t.totalRows)+" 条 ",1)):p("",!0),ve(t)?(u(),c("div",Zs,[o("div",et,[s[9]||(s[9]=o("span",null,"任务进度",-1)),o("span",null,f(X(t))+"%",1)]),o("div",st,[o("div",{class:"delete-brand-progress-bar-fill",style:Fe({width:`${X(t)}%`})},null,4)]),o("div",tt,f(_e(t)),1)])):p("",!0),t.previewRows?.length?(u(),c("div",nt,f(be(t.previewRows)),1)):p("",!0),Q(t)?(u(),c("div",at,"错误信息:"+f(Q(t)),1)):p("",!0)]),o("div",lt,[o("span",{class:Ne(["status",W(t).className])},f(W(t).text),3),ye(t)?(u(),c("button",{key:0,type:"button",class:"download",onClick:n=>we(t)}," 下载 ",8,ot)):p("",!0),t.resultId?(u(),c("button",{key:1,type:"button",class:"btn-delete",onClick:n=>Se(t.resultId)}," 删除 ",8,rt)):p("",!0)])]))),128))])])):p("",!0)],64)):(u(),c("div",Ss," 暂无删除品牌结果,完成解析后会在这里展示按国家分组的去重结果 "))])])])])]))}}),ut=Ze(it,[["__scopeId","data-v-d37369fa"]]);es(ut).use(ss).mount("#app"); diff --git a/app/main.py b/app/main.py index 1c561fb..3cc2e65 100644 --- a/app/main.py +++ b/app/main.py @@ -154,7 +154,7 @@ class WindowAPI: ) return result[0] if result else '' - def save_file_from_url(self, url, default_filename='download.bin'): + def save_file_from_url_new(self, url, default_filename='download.bin'): """弹窗选择保存位置,从 url 下载文件并保存。根据文件后缀动态设置保存类型。""" if not url or not url.strip(): return {'success': False, 'error': '下载地址为空'} @@ -360,7 +360,7 @@ def main(): ) api = WindowAPI(window) window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, - api.save_file_from_url, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del) + api.save_file_from_url_new, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del) webview.start( debug=True, storage_path=cache_path, diff --git a/app/new_web_source/delete-brand.html b/app/new_web_source/delete-brand.html index b3e2adf..40e844b 100644 --- a/app/new_web_source/delete-brand.html +++ b/app/new_web_source/delete-brand.html @@ -8,7 +8,7 @@ - +
diff --git a/app/web_source/brand-旧.html b/app/web_source/brand-旧.html index 871c8f7..9e4f06e 100644 --- a/app/web_source/brand-旧.html +++ b/app/web_source/brand-旧.html @@ -927,7 +927,7 @@ } var zipUrl = task.result_paths.zip_url; var filename = 'brand_task_' + taskId + '.zip'; - if (api && api.save_file_from_url) { + if (api && api.save_file_from_url_new) { a.style.pointerEvents = 'none'; var done = function(ret) { if (ret && ret.success) { @@ -938,7 +938,7 @@ a.style.pointerEvents = ''; }; try { - var ret = api.save_file_from_url(zipUrl, filename); + var ret = api.save_file_from_url_new(zipUrl, filename); if (ret && typeof ret.then === 'function') { ret.then(done).catch(function(err) { showToast('保存失败:' + (err && err.message || err)); a.style.pointerEvents = ''; }); } else { diff --git a/app/web_source/brand.html b/app/web_source/brand.html index 9c0c7df..8966292 100644 --- a/app/web_source/brand.html +++ b/app/web_source/brand.html @@ -995,7 +995,7 @@ } var zipUrl = task.result_paths.zip_url; var filename = 'brand_task_' + taskId + '.zip'; - if (api && api.save_file_from_url) { + if (api && api.save_file_from_url_new) { a.style.pointerEvents = 'none'; var done = function(ret) { if (ret && ret.success) { @@ -1006,7 +1006,7 @@ a.style.pointerEvents = ''; }; try { - var ret = api.save_file_from_url(zipUrl, filename); + var ret = api.save_file_from_url_new(zipUrl, filename); if (ret && typeof ret.then === 'function') { ret.then(done).catch(function(err) { showToast('保存失败:' + (err && err.message || err)); a.style.pointerEvents = ''; }); } else { diff --git a/app/web_source/templates_backup/brand.html b/app/web_source/templates_backup/brand.html index f5afd87..86ea7de 100644 --- a/app/web_source/templates_backup/brand.html +++ b/app/web_source/templates_backup/brand.html @@ -877,7 +877,7 @@ } var zipUrl = task.result_paths.zip_url; var filename = 'brand_task_' + taskId + '.zip'; - if (api && api.save_file_from_url) { + if (api && api.save_file_from_url_new) { a.style.pointerEvents = 'none'; var done = function(ret) { if (ret && ret.success) { @@ -888,7 +888,7 @@ a.style.pointerEvents = ''; }; try { - var ret = api.save_file_from_url(zipUrl, filename); + var ret = api.save_file_from_url_new(zipUrl, filename); if (ret && typeof ret.then === 'function') { ret.then(done).catch(function(err) { showToast('保存失败:' + (err && err.message || err)); a.style.pointerEvents = ''; }); } else { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java index 2ffc659..0fff6e1 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/controller/ConvertRunController.java @@ -95,7 +95,7 @@ public class ConvertRunController { } @GetMapping("/results/{resultId}/download") - @Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载当前用户的转换结果文件。桌面端前端可继续通过 save_file_from_url 选择保存位置。") + @Operation(summary = "下载格式转换结果", description = "根据结果记录 ID 下载当前用户的转换结果文件。桌面端前端可继续通过 save_file_from_url_new 选择保存位置。") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "返回结果文件流"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "结果文件不存在") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java index 157ef90..54e0a83 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java @@ -103,7 +103,7 @@ public class DeleteBrandRunController { } @GetMapping("/tasks/{taskId}/download") - @Operation(summary = "下载删除品牌任务结果", description = "返回任务结果文件流,便于桌面端通过 save_file_from_url 直接保存。") + @Operation(summary = "下载删除品牌任务结果", description = "返回任务结果文件流,便于桌面端通过 save_file_from_url_new 直接保存。") public void download(@PathVariable Long taskId, jakarta.servlet.http.HttpServletResponse response) { String url = deleteBrandRunService.resolveTaskDownloadUrl(taskId); String filename = deleteBrandRunService.resolveTaskDownloadFilename(taskId); diff --git a/frontend-vue/src/pages/brand/components/BrandConvertTab.vue b/frontend-vue/src/pages/brand/components/BrandConvertTab.vue index 5172082..cedd88a 100644 --- a/frontend-vue/src/pages/brand/components/BrandConvertTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandConvertTab.vue @@ -391,8 +391,8 @@ async function saveTemplateFromUrl(url: string, fallbackFilename: string, bridge return } - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(url, fallbackFilename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(url, fallbackFilename) if (result.success) { ElMessage.success(`已保存:${result.path || fallbackFilename}`) } else if (result.error && result.error !== '用户取消') { @@ -480,8 +480,8 @@ async function downloadConvertResult(item: ConvertResultItem) { } const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.txt` - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(item.downloadUrl, filename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(item.downloadUrl, filename) if (result.success) { ElMessage.success(`已保存:${result.path || filename}`) } else if (result.error && result.error !== '用户取消') { diff --git a/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue b/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue index a6cc2cd..2c7e4ec 100644 --- a/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandDedupeTab.vue @@ -335,8 +335,8 @@ async function downloadCleanResult(item: DedupeResultItem) { } const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}_cleaned.xlsx` - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(item.downloadUrl, filename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(item.downloadUrl, filename) if (result.success) { ElMessage.success(`已保存:${result.path || filename}`) } else if (result.error && result.error !== '用户取消') { diff --git a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue index 07f3cb5..d3d0c91 100644 --- a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue @@ -286,22 +286,52 @@ function loadSessionTasksFromStorage() { } function saveSessionTask(taskId: number, items: DeleteBrandResultItem[], pushResult?: string, payloadText?: string) { - const validItems = items.filter(i => i.matchStatus === 'MATCHED' && i.success !== false) - if (validItems.length === 0) return + if (!items?.length) return + + const existingIndex = sessionTasks.value.findIndex(t => t.taskId === taskId) + const existingTask = existingIndex >= 0 ? sessionTasks.value[existingIndex] : null + + // 全量保留任务项(不在存储层按 matched/success 过滤),避免单任务多店铺时出现“未处理项丢失”。 + // 同时尽量继承已有的 _pushed/_completed 会话态。 + const mergedItems = items.map((item) => { + const prev = existingTask?.items.find((i) => isSameDeleteBrandItem(i, item)) + + // 保护大字段:当新值为空数组时,保留本地已有的非空数据,避免被轮询详情的精简 payload 覆盖。 + const nextCountries = Array.isArray(item.countries) ? item.countries : undefined + const prevCountries = Array.isArray(prev?.countries) ? prev?.countries : undefined + const countries = (nextCountries && nextCountries.length > 0) + ? nextCountries + : (prevCountries && prevCountries.length > 0 ? prevCountries : (nextCountries ?? prevCountries)) + + const nextPreviewRows = Array.isArray(item.previewRows) ? item.previewRows : undefined + const prevPreviewRows = Array.isArray(prev?.previewRows) ? prev?.previewRows : undefined + const previewRows = (nextPreviewRows && nextPreviewRows.length > 0) + ? nextPreviewRows + : (prevPreviewRows && prevPreviewRows.length > 0 ? prevPreviewRows : (nextPreviewRows ?? prevPreviewRows)) + + return { + ...item, + countries, + previewRows, + _pushed: prev?._pushed ?? (item as SessionDeleteBrandItem)._pushed ?? false, + _completed: prev?._completed ?? (item as SessionDeleteBrandItem)._completed ?? false, + } as SessionDeleteBrandItem + }) const newTask: StoredCurrentTask = { taskId, - items: validItems, - createdAt: Date.now(), - queuePushResult: pushResult, - queuePayloadText: payloadText + items: mergedItems, + createdAt: existingTask?.createdAt || Date.now(), + queuePushResult: pushResult ?? existingTask?.queuePushResult, + queuePayloadText: payloadText ?? existingTask?.queuePayloadText, } - const existingIndex = sessionTasks.value.findIndex(t => t.taskId === taskId) + if (existingIndex >= 0) { sessionTasks.value[existingIndex] = newTask } else { sessionTasks.value.push(newTask) } + if (typeof window !== 'undefined') { window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value)) } @@ -318,7 +348,7 @@ function removeSessionTaskFromStorage(taskId: number) { const historySectionItems = computed(() => historyItems.value.filter( - (item) => !currentSectionItems.value.some(c => c.taskId === item.taskId || (c.resultId && c.resultId === item.resultId)) + (item) => !currentSectionItems.value.some(c => isSameDeleteBrandItem(c, item)) ), ); const hasVisibleItems = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0) @@ -366,30 +396,28 @@ function updateCompletedItemsFromProgress(taskId: number | undefined) { let changed = false const info = taskDetails.value[taskId]?.line_progress?.info + const multiFileTask = isMultiFileTask(taskId) - // 多文件任务优先依据后端 finished_files 同步已完成文件数量。 - // 规则:按 sessionTask.items 的顺序,标记「已推送」中的前 N 个为 completed。 - if (isMultiFileTask(taskId) && info?.finished_files != null && info.finished_files >= 0) { - const pushedItems = sessionTask.items.filter(i => i._pushed) - const shouldCompleted = Math.max(0, Math.min(info.finished_files, pushedItems.length)) - - for (let i = 0; i < shouldCompleted; i += 1) { - if (!pushedItems[i]._completed) { - pushedItems[i]._completed = true + // 关键修复:多文件任务下不再用 finished_files 按顺序回填 completed。 + // 否则当第 1 个文件完成后,推入第 2 个文件时会因为 finished_files=1 被误判为“本文件已完成(100%)”。 + // 改为仅当后端进度明确指向该文件且 current_line >= total_lines 时,才标记该文件 completed。 + if (info?.file_name && info.current_line != null && info.total_lines != null && info.total_lines > 0) { + if (info.current_line >= info.total_lines) { + const sessionItem = sessionTask.items.find(i => i.sourceFilename === info.file_name) + if (sessionItem && !sessionItem._completed) { + sessionItem._completed = true changed = true } } } - // 兼容单文件/兜底:当当前文件行进度到达末行时,标记活动文件完成。 - if (info?.file_name && info.current_line != null && info.total_lines != null && info.total_lines > 0) { - if (info.current_line >= info.total_lines) { - const activeItem = currentSectionItems.value.find(item => getItemKey(item) === activeItemKey.value) - const sessionItem = activeItem && activeItem.taskId === taskId - ? findSessionItem(taskId, activeItem) - : sessionTask.items.find(i => i.sourceFilename === info.file_name) - if (sessionItem && !sessionItem._completed) { - sessionItem._completed = true + // 单文件任务兜底:任务终态时标记完成,避免偶发进度丢包导致状态卡住。 + if (!multiFileTask) { + const status = taskDetails.value[taskId]?.task?.status + if ((status === 'SUCCESS' || status === 'FAILED') && sessionTask.items.length === 1) { + const onlyItem = sessionTask.items[0] + if (onlyItem && !onlyItem._completed) { + onlyItem._completed = true changed = true } } @@ -531,6 +559,48 @@ function formatProgress(item: DeleteBrandResultItem) { const detail = taskDetails.value[taskId] const info = detail?.line_progress?.info const fp = getFileProgress(item) + const multiFileTask = isMultiFileTask(taskId) + + // 多文件任务优先按“当前运行文件 + 本地会话态”展示,避免后端 finished_files 按顺序分配导致某个文件被误判 COMPLETED。 + if (multiFileTask) { + const sessionItem = findSessionItem(taskId, item) + const status = detail?.task?.status + const isRunningTarget = Boolean(info?.file_name && info.file_name === item.sourceFilename) + + let displayStatus = sessionItem?._completed ? 'COMPLETED' : (sessionItem?._pushed ? 'PENDING' : 'PENDING') + let processedRows = 0 + let totalRows = Math.max(0, item.totalRows ?? fp?.totalRows ?? 0) + + if (status === 'SUCCESS') { + displayStatus = 'COMPLETED' + processedRows = totalRows + } else if (status === 'FAILED' && sessionItem?._completed) { + displayStatus = 'COMPLETED' + processedRows = totalRows + } else if (isRunningTarget) { + displayStatus = 'RUNNING' + processedRows = Math.max(0, Math.min(totalRows || Number.MAX_SAFE_INTEGER, info?.current_line ?? 0)) + if (!totalRows && info?.total_lines && info.total_lines > 0) { + totalRows = info.total_lines + } + } else if (sessionItem?._completed) { + displayStatus = 'COMPLETED' + processedRows = totalRows + } else if (sessionItem?._pushed) { + displayStatus = 'PENDING' + processedRows = 0 + } + + const parts: string[] = [] + parts.push(`文件状态:${displayStatus}`) + parts.push(`文件进度:${processedRows}/${totalRows}`) + if (info?.finished_files != null) { + const total = info.file_total && info.file_total > 0 ? `/${info.file_total}` : '' + parts.push(`已完成文件:${info.finished_files}${total}`) + } + if (info?.phase) parts.push(`阶段:${info.phase}`) + return parts.join('|') + } if (fp) { const parts: string[] = [] @@ -571,12 +641,32 @@ function formatProgressPercent(item: DeleteBrandResultItem) { const taskId = item.taskId if (!taskId) return 0 + const info = taskDetails.value[taskId]?.line_progress?.info + const multiFileTask = isMultiFileTask(taskId) + + if (multiFileTask) { + const sessionItem = findSessionItem(taskId, item) + const taskStatus = taskDetails.value[taskId]?.task?.status + if (taskStatus === 'SUCCESS' || sessionItem?._completed) return 100 + + const totalRows = Math.max(0, item.totalRows ?? getFileProgress(item)?.totalRows ?? 0) + const isRunningTarget = Boolean(info?.file_name && info.file_name === item.sourceFilename) + if (isRunningTarget) { + if (info?.current_line != null && info?.total_lines && info.total_lines > 0) { + return Math.max(0, Math.min(100, Math.round((info.current_line / info.total_lines) * 100))) + } + if (info?.current_line != null && totalRows > 0) { + return Math.max(0, Math.min(100, Math.round((info.current_line / totalRows) * 100))) + } + } + return 0 + } + const fp = getFileProgress(item) if (fp) { return Math.max(0, Math.min(100, fp.percent ?? 0)) } - const info = taskDetails.value[taskId]?.line_progress?.info if (!info) return 0 if (info.current_line != null && info.total_lines && info.total_lines > 0) { @@ -667,30 +757,58 @@ function canDownloadTaskResult(item: DeleteBrandResultItem) { function mergeTaskDetailItemsIntoSession(detail: DeleteBrandTaskDetailVo) { const taskId = detail?.task?.id - if (!taskId || !detail?.items?.length) return false + if (!taskId) return false const sessionTask = sessionTasks.value.find(t => t.taskId === taskId) if (!sessionTask) return false + const detailItems = detail?.items || [] let changed = false + + // 以本地会话项为主做增量合并,保留 _pushed/_completed; + // 对后端新增返回的项追加到末尾,避免单任务多店铺/后续补齐时丢项。 const mergedItems = sessionTask.items.map((item) => { - const latest = detail.items?.find((candidate) => isSameDeleteBrandItem(candidate, item)) + const latest = detailItems.find((candidate) => isSameDeleteBrandItem(candidate, item)) if (!latest) return item + + const latestCountries = Array.isArray(latest.countries) ? latest.countries : undefined + const itemCountries = Array.isArray(item.countries) ? item.countries : undefined + const countries = (latestCountries && latestCountries.length > 0) + ? latestCountries + : (itemCountries && itemCountries.length > 0 ? itemCountries : (latestCountries ?? itemCountries)) + + const latestPreviewRows = Array.isArray(latest.previewRows) ? latest.previewRows : undefined + const itemPreviewRows = Array.isArray(item.previewRows) ? item.previewRows : undefined + const previewRows = (latestPreviewRows && latestPreviewRows.length > 0) + ? latestPreviewRows + : (itemPreviewRows && itemPreviewRows.length > 0 ? itemPreviewRows : (latestPreviewRows ?? itemPreviewRows)) + const merged = { ...item, ...latest, - success: item.success ?? latest.success, + countries, + previewRows, _pushed: item._pushed, _completed: item._completed, - } + } as SessionDeleteBrandItem if (JSON.stringify(merged) !== JSON.stringify(item)) { changed = true } return merged }) - const validItems = mergedItems.filter(i => i.matchStatus === 'MATCHED' && i.success !== false) - if (JSON.stringify(validItems) !== JSON.stringify(sessionTask.items)) { - sessionTask.items = validItems + for (const latest of detailItems) { + if (!mergedItems.some((item) => isSameDeleteBrandItem(item, latest))) { + mergedItems.push({ + ...latest, + _pushed: false, + _completed: false, + } as SessionDeleteBrandItem) + changed = true + } + } + + if (JSON.stringify(mergedItems) !== JSON.stringify(sessionTask.items)) { + sessionTask.items = mergedItems changed = true } @@ -750,21 +868,26 @@ function scheduleNextPoll(immediate = false) { pollingInFlight.value = true let stateChanged = false + let shouldRefreshHistory = false try { await refreshTaskDetails(taskIds) await maybeAutoAdvance() for (const taskId of taskIds) { if (isTaskTerminal(taskId)) { + // 以后端任务终态为准:进入 SUCCESS/FAILED 后立即清理本地当前任务并刷新 history, + // 避免 _completed 会话态偶发不同步导致持续轮询 batch。 removeSessionTaskFromStorage(taskId) stateChanged = true + shouldRefreshHistory = true } } } finally { pollingInFlight.value = false } - if (stateChanged) { - loadHistory() + if (shouldRefreshHistory) { + await loadHistory() + } else if (stateChanged) { syncResultState() } @@ -923,8 +1046,8 @@ async function downloadTaskResult(item: DeleteBrandResultItem) { item.sourceFilename || `delete-brand_${taskId}.xlsx` - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(url, filename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(url, filename) if (result.success) { ElMessage.success(`已保存:${result.path || filename}`) } else if (result.error && result.error !== '用户取消') { @@ -1174,15 +1297,20 @@ onMounted(() => { loadSessionTasksFromStorage() const taskIds = getPollingTaskIds() if (taskIds.length > 0) { - refreshTaskDetails(taskIds).then(() => { + refreshTaskDetails(taskIds).then(async () => { let stateChanged = false + let shouldRefreshHistory = false for (const taskId of taskIds) { if (isTaskTerminal(taskId)) { + // 首屏恢复也以后端终态为准,避免依赖 _completed 造成卡轮询。 removeSessionTaskFromStorage(taskId) stateChanged = true + shouldRefreshHistory = true } } - if (stateChanged) { + if (shouldRefreshHistory) { + await loadHistory() + } else if (stateChanged) { syncResultState() } ensurePolling(true) diff --git a/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue b/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue index 2fca269..124ac85 100644 --- a/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandProductRiskTab.vue @@ -18,13 +18,8 @@
备选区域
暂无备选店铺,请先输入并确认
- + @@ -40,34 +35,18 @@ 勾选需要处理的站点(可只选 2~3 国);上方勾选区先按已选顺序排列,其余未选国家排在后面。拖拽下方列表调整顺序后会与上方同步。设置会按当前登录用户保存。

-
已选顺序(拖拽 ⋮⋮ 调整)
-
+
⋮⋮ {{ countryLabel(code) }}({{ code }})
@@ -78,18 +57,9 @@
商品列表筛选

与「推送到 Python 队列」一并下发,供 Python 区分处理场景。

- - + +
@@ -97,16 +67,15 @@ -
-

与删除 ASIN 品牌相同:先创建任务,再按店铺逐条入队(每条 data.items 仅含一个店铺)。队列串行执行;Python 每处理完一店可 POST /api/product-risk-resolve/tasks/{taskId}/result(可多次,每次可只含已完成的店铺);全部完成后任务结束。前端轮询任务状态。

+

与删除 ASIN 品牌相同:先创建任务,再按店铺逐条入队(每条 data.items + 仅含一个店铺)。队列串行执行;Python + 每处理完一店可 POST /api/product-risk-resolve/tasks/{taskId}/result(可多次,每次可只含已完成的店铺);全部完成后任务结束。前端轮询任务状态。 +

推送结果
@@ -139,13 +108,8 @@
匹配结果
完成「匹配店铺」后,在此展示紫鸟索引匹配结果,供核对后再推送队列。
- + @@ -360,8 +354,8 @@ async function downloadSplitResult(item: SplitResultItem) { } const filename = item.outputFilename || `${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.zip` - if (api?.save_file_from_url) { - const result = await api.save_file_from_url(item.downloadUrl, filename) + if (api?.save_file_from_url_new) { + const result = await api.save_file_from_url_new(item.downloadUrl, filename) if (result.success) { ElMessage.success(`已保存:${result.path || filename}`) } else if (result.error && result.error !== '用户取消') { @@ -379,75 +373,516 @@ onMounted(() => { diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 2cffca2..a4a4833 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -1,461 +1,581 @@ -import { del, get, http, post, put, type JavaApiResponse, unwrapJavaResponse } from '@/shared/api/http' +import { + del, + get, + http, + post, + put, + type JavaApiResponse, + unwrapJavaResponse, +} from "@/shared/api/http"; -const JAVA_API_PREFIX = '/newApi/api' +const JAVA_API_PREFIX = "/newApi/api"; function getCurrentUserId() { - const raw = typeof window === 'undefined' ? '' : window.localStorage.getItem('uid') || '' - const value = Number(raw) + const raw = + typeof window === "undefined" + ? "" + : window.localStorage.getItem("uid") || ""; + const value = Number(raw); if (!Number.isFinite(value) || value <= 0) { - throw new Error('未获取到用户ID') + throw new Error("未获取到用户ID"); } - return value + return value; } export interface UploadedFileRef { - fileKey: string - originalFilename?: string - relativePath?: string + fileKey: string; + originalFilename?: string; + relativePath?: string; } export interface UploadFileVo { - fileKey: string - originalFilename: string - localPath: string - size: number - relativePath?: string + fileKey: string; + originalFilename: string; + localPath: string; + size: number; + relativePath?: string; } export interface DedupeResultItem { - resultId?: number - sourceFilename: string - outputFilename?: string - success: boolean - error?: string - downloadUrl?: string + resultId?: number; + sourceFilename: string; + outputFilename?: string; + success: boolean; + error?: string; + downloadUrl?: string; } export interface DedupeRunVo { - total: number - successCount: number - failedCount: number - items: DedupeResultItem[] + total: number; + successCount: number; + failedCount: number; + items: DedupeResultItem[]; } export interface DedupeHistoryVo { - items: DedupeResultItem[] + items: DedupeResultItem[]; } export interface DedupeRunRequest { - files: UploadedFileRef[] - selectedColumns: string[] - keepIntegerIds: boolean - keepUnderscoreIds: boolean - keepIntegerMainIdsWhenNoSubIds: boolean - archiveName?: string - user_id: number + files: UploadedFileRef[]; + selectedColumns: string[]; + keepIntegerIds: boolean; + keepUnderscoreIds: boolean; + keepIntegerMainIdsWhenNoSubIds: boolean; + archiveName?: string; + user_id: number; } export interface SplitArchiveEntry { - filename: string - rowCount?: number + filename: string; + rowCount?: number; } export interface SplitResultItem { - resultId?: number - sourceFilename: string - outputFilename?: string - success: boolean - error?: string - rowCount?: number - downloadUrl?: string - entryCount?: number - entries?: SplitArchiveEntry[] + resultId?: number; + sourceFilename: string; + outputFilename?: string; + success: boolean; + error?: string; + rowCount?: number; + downloadUrl?: string; + entryCount?: number; + entries?: SplitArchiveEntry[]; } export interface SplitRunVo { - total: number - successCount: number - failedCount: number - items: SplitResultItem[] + total: number; + successCount: number; + failedCount: number; + items: SplitResultItem[]; } export interface SplitHistoryVo { - items: SplitResultItem[] + items: SplitResultItem[]; } export interface SplitRunRequest { - files: UploadedFileRef[] - selectedColumns: string[] - splitMode: 'rows_per_file' | 'parts' - rowsPerFile?: number - parts?: number - archiveName?: string - user_id: number + files: UploadedFileRef[]; + selectedColumns: string[]; + splitMode: "rows_per_file" | "parts"; + rowsPerFile?: number; + parts?: number; + archiveName?: string; + user_id: number; } export interface ConvertResultItem { - resultId?: number - sourceFilename: string - outputFilename?: string - success: boolean - error?: string - downloadUrl?: string + resultId?: number; + sourceFilename: string; + outputFilename?: string; + success: boolean; + error?: string; + downloadUrl?: string; } export interface ConvertRunVo { - total: number - successCount: number - failedCount: number - items: ConvertResultItem[] + total: number; + successCount: number; + failedCount: number; + items: ConvertResultItem[]; } export interface ConvertHistoryVo { - items: ConvertResultItem[] + items: ConvertResultItem[]; } export interface ConvertRunRequest { - files: UploadedFileRef[] - templateId: string - archiveName?: string - user_id: number + files: UploadedFileRef[]; + templateId: string; + archiveName?: string; + user_id: number; } export interface DeleteBrandPreviewRow { - rowIndex: number - country: string - asin: string - status: string + rowIndex: number; + country: string; + asin: string; + status: string; } export interface DeleteBrandCountryAsinItem { - rowIndex: number - asin: string - status: string + rowIndex: number; + asin: string; + status: string; } export interface DeleteBrandCountryGroup { - country: string - asinCount: number - items: DeleteBrandCountryAsinItem[] + country: string; + asinCount: number; + items: DeleteBrandCountryAsinItem[]; } export interface DeleteBrandCountryResultItem { - asin: string - status?: string + asin: string; + status?: string; } export interface DeleteBrandProcessedCountry { - country: string - items: DeleteBrandCountryResultItem[] + country: string; + items: DeleteBrandCountryResultItem[]; } export interface DeleteBrandResultFile { - fileKey?: string - sourceFilename: string - fileIndex?: number - fileTotal?: number - chunkIndex?: number - chunkTotal?: number - processedRows?: number - totalRows?: number - currentCountry?: string - currentAsin?: string - countries: DeleteBrandProcessedCountry[] + fileKey?: string; + sourceFilename: string; + fileIndex?: number; + fileTotal?: number; + chunkIndex?: number; + chunkTotal?: number; + processedRows?: number; + totalRows?: number; + currentCountry?: string; + currentAsin?: string; + countries: DeleteBrandProcessedCountry[]; } export interface DeleteBrandSubmitResultRequest { - submissionId?: string - files: DeleteBrandResultFile[] + submissionId?: string; + files: DeleteBrandResultFile[]; } export interface DeleteBrandLineProgressInfo { - file_index?: number - file_total?: number - file_name?: string - current_line?: number - total_lines?: number - current_country?: string - current_asin?: string - finished_files?: number - phase?: string + file_index?: number; + file_total?: number; + file_name?: string; + current_line?: number; + total_lines?: number; + current_country?: string; + current_asin?: string; + finished_files?: number; + phase?: string; } export interface DeleteBrandLineProgress { - has_progress: boolean - info?: DeleteBrandLineProgressInfo + has_progress: boolean; + info?: DeleteBrandLineProgressInfo; } export interface DeleteBrandTaskItem { - id: number - taskNo?: string - status?: string - sourceFileCount?: number - successFileCount?: number - failedFileCount?: number - errorMessage?: string - createdAt?: string - updatedAt?: string - finishedAt?: string - downloadUrl?: string - downloadFilename?: string + id: number; + taskNo?: string; + status?: string; + sourceFileCount?: number; + successFileCount?: number; + failedFileCount?: number; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; + downloadUrl?: string; + downloadFilename?: string; } export interface DeleteBrandTaskFileProgress { - fileKey?: string - sourceFilename?: string - processedRows?: number - totalRows?: number - percent?: number - status?: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' + fileKey?: string; + sourceFilename?: string; + processedRows?: number; + totalRows?: number; + percent?: number; + status?: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED"; } export interface DeleteBrandTaskDetailVo { - task: DeleteBrandTaskItem - line_progress: DeleteBrandLineProgress - items?: DeleteBrandResultItem[] - fileProgress?: DeleteBrandTaskFileProgress[] + task: DeleteBrandTaskItem; + line_progress: DeleteBrandLineProgress; + items?: DeleteBrandResultItem[]; + fileProgress?: DeleteBrandTaskFileProgress[]; } export interface DeleteBrandTaskBatchVo { - items: DeleteBrandTaskDetailVo[] - missingTaskIds: number[] + items: DeleteBrandTaskDetailVo[]; + missingTaskIds: number[]; } -export type DeleteBrandMatchStatus = 'MATCHED' | 'PENDING' | 'CONFLICT' | 'INDEX_STALE' +export type DeleteBrandMatchStatus = + | "MATCHED" + | "PENDING" + | "CONFLICT" + | "INDEX_STALE"; export interface DeleteBrandResultItem { - resultId?: number - fileKey?: string - sourceFilename: string - shopName?: string - matched?: boolean - matchStatus?: DeleteBrandMatchStatus - matchMessage?: string - shopId?: string - platform?: string - openStoreUrl?: string - outputFilename?: string - downloadUrl?: string - taskId?: number - countryCount?: number - countries?: DeleteBrandCountryGroup[] - totalRows?: number - truncated?: boolean - previewRows?: DeleteBrandPreviewRow[] - success: boolean - error?: string - taskStatus?: string + resultId?: number; + fileKey?: string; + sourceFilename: string; + shopName?: string; + matched?: boolean; + matchStatus?: DeleteBrandMatchStatus; + matchMessage?: string; + shopId?: string; + platform?: string; + openStoreUrl?: string; + outputFilename?: string; + downloadUrl?: string; + taskId?: number; + countryCount?: number; + countries?: DeleteBrandCountryGroup[]; + totalRows?: number; + truncated?: boolean; + previewRows?: DeleteBrandPreviewRow[]; + success: boolean; + error?: string; + taskStatus?: string; } export interface DeleteBrandRunVo { - total: number - successCount: number - failedCount: number - items: DeleteBrandResultItem[] + total: number; + successCount: number; + failedCount: number; + items: DeleteBrandResultItem[]; } export interface DeleteBrandHistoryVo { - items: DeleteBrandResultItem[] + items: DeleteBrandResultItem[]; } export interface DeleteBrandRunRequest { - files: UploadedFileRef[] - user_id: number + files: UploadedFileRef[]; + user_id: number; } export interface ConvertTemplateVo { - id: string - templateCode: string - templateName: string - outputFilename: string - isDefault?: boolean - builtIn?: boolean + id: string; + templateCode: string; + templateName: string; + outputFilename: string; + isDefault?: boolean; + builtIn?: boolean; } export interface ConvertTemplateImportRequest { - templateName: string - templateContent: string + templateName: string; + templateContent: string; } export interface ExcelInfoVo { - headers: string[] - totalRows: number + headers: string[]; + totalRows: number; } export function getExcelInfo(fileKey: string) { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/files/excel-info`, { - params: { fileKey }, - })) + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/files/excel-info`, { + params: { fileKey }, + }), + ); } -export function runDedupe(request: Omit | DedupeRunRequest) { - return unwrapJavaResponse(post, DedupeRunRequest>(`${JAVA_API_PREFIX}/dedupe/run`, { - ...request, - user_id: getCurrentUserId(), - })) +export function runDedupe( + request: Omit | DedupeRunRequest, +) { + return unwrapJavaResponse( + post, DedupeRunRequest>( + `${JAVA_API_PREFIX}/dedupe/run`, + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); } export function getDedupeHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/dedupe/history`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/dedupe/history`, { + params: { user_id: getCurrentUserId() }, + }), + ); } export function deleteDedupeHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/dedupe/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/dedupe/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } -export function runSplit(request: Omit | SplitRunRequest) { - return unwrapJavaResponse(post, SplitRunRequest>(`${JAVA_API_PREFIX}/split/run`, { - ...request, - user_id: getCurrentUserId(), - })) +export function runSplit( + request: Omit | SplitRunRequest, +) { + return unwrapJavaResponse( + post, SplitRunRequest>( + `${JAVA_API_PREFIX}/split/run`, + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); } export function getSplitHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/split/history`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/split/history`, { + params: { user_id: getCurrentUserId() }, + }), + ); } export function deleteSplitHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/split/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/split/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); } export function getConvertTemplates() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/convert/templates`)) + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/convert/templates`, + ), + ); } export function importConvertTemplate(request: ConvertTemplateImportRequest) { return unwrapJavaResponse( - post, ConvertTemplateImportRequest>(`${JAVA_API_PREFIX}/convert/templates/import`, request), - ) + post, ConvertTemplateImportRequest>( + `${JAVA_API_PREFIX}/convert/templates/import`, + request, + ), + ); } export function setDefaultConvertTemplate(templateCode: string) { - return unwrapJavaResponse(post>(`${JAVA_API_PREFIX}/convert/templates/${templateCode}/default`)) + return unwrapJavaResponse( + post>( + `${JAVA_API_PREFIX}/convert/templates/${templateCode}/default`, + ), + ); } export function deleteConvertTemplate(templateCode: string) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/convert/templates/${templateCode}`)) + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/convert/templates/${templateCode}`, + ), + ); } -export function runConvert(request: Omit | ConvertRunRequest) { - return unwrapJavaResponse(post, ConvertRunRequest>(`${JAVA_API_PREFIX}/convert/run`, { - ...request, - user_id: getCurrentUserId(), - })) +export function runConvert( + request: Omit | ConvertRunRequest, +) { + return unwrapJavaResponse( + post, ConvertRunRequest>( + `${JAVA_API_PREFIX}/convert/run`, + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); } export function getConvertHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/convert/history`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/convert/history`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function deleteConvertHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/convert/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/convert/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } -export function runDeleteBrand(request: Omit | DeleteBrandRunRequest) { - return unwrapJavaResponse(post, DeleteBrandRunRequest>(`${JAVA_API_PREFIX}/delete-brand/run`, { - ...request, - user_id: getCurrentUserId(), - })) +export function runDeleteBrand( + request: Omit | DeleteBrandRunRequest, +) { + return unwrapJavaResponse( + post, DeleteBrandRunRequest>( + `${JAVA_API_PREFIX}/delete-brand/run`, + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); } export function getDeleteBrandHistory() { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/delete-brand/history`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/delete-brand/history`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function deleteDeleteBrandHistory(resultId: number) { - return unwrapJavaResponse(del>(`${JAVA_API_PREFIX}/delete-brand/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - })) + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/delete-brand/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function getDeleteBrandTaskDetail(taskId: number) { - return unwrapJavaResponse(get>(`${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}`)) + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}`, + ), + ); } export function getDeleteBrandTaskDetails(taskIds: number[]) { - return unwrapJavaResponse(post, { taskIds: number[] }>(`${JAVA_API_PREFIX}/delete-brand/tasks/batch`, { taskIds })) + return unwrapJavaResponse( + post, { taskIds: number[] }>( + `${JAVA_API_PREFIX}/delete-brand/tasks/batch`, + { taskIds }, + ), + ); } export function getDeleteBrandTaskDownloadUrl(taskId: number) { - return getJavaDownloadUrl(`/delete-brand/tasks/${taskId}/download`) + return getJavaDownloadUrl(`/delete-brand/tasks/${taskId}/download`); } -export function submitDeleteBrandResult(taskId: number, request: DeleteBrandSubmitResultRequest) { - return unwrapJavaResponse(post, DeleteBrandSubmitResultRequest>(`${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}/result`, request)) +export function submitDeleteBrandResult( + taskId: number, + request: DeleteBrandSubmitResultRequest, +) { + return unwrapJavaResponse( + post, DeleteBrandSubmitResultRequest>( + `${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}/result`, + request, + ), + ); } /** 商品风险解决:备选店铺与匹配(推队列由前端 pywebview 完成) */ export interface ProductRiskCandidateVo { - id: number - shop_name: string - created_at?: string + id: number; + shop_name: string; + created_at?: string; } export interface ProductRiskShopQueueItem { - shopName: string - matched: boolean - shopId?: string - platform?: string - companyName?: string - openStoreUrl?: string - matchedUserId?: number - matchStatus?: string - matchMessage?: string + shopName: string; + matched: boolean; + shopId?: string; + platform?: string; + companyName?: string; + openStoreUrl?: string; + matchedUserId?: number; + matchStatus?: string; + matchMessage?: string; } export interface ProductRiskMatchShopsVo { - items: ProductRiskShopQueueItem[] + items: ProductRiskShopQueueItem[]; } export function listProductRiskCandidates() { return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/product-risk-resolve/candidates`, { - params: { user_id: getCurrentUserId() }, - }), - ) + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/candidates`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function addProductRiskCandidate(shopName: string) { return unwrapJavaResponse( - post, { user_id: number; shop_name: string }>( - `${JAVA_API_PREFIX}/product-risk-resolve/candidates`, - { user_id: getCurrentUserId(), shop_name: shopName }, - ), - ) + post< + JavaApiResponse, + { user_id: number; shop_name: string } + >(`${JAVA_API_PREFIX}/product-risk-resolve/candidates`, { + user_id: getCurrentUserId(), + shop_name: shopName, + }), + ); } export function deleteProductRiskCandidate(id: number) { return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/product-risk-resolve/candidates/${id}`, { - params: { user_id: getCurrentUserId() }, - }), - ) + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/candidates/${id}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export interface ProductRiskCountryPreferenceVo { - country_codes: string[] + country_codes: string[]; } export function getProductRiskCountryPreference() { return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/product-risk-resolve/country-preference`, { - params: { user_id: getCurrentUserId() }, - }), - ) + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/country-preference`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function putProductRiskCountryPreference(countryCodes: string[]) { @@ -467,122 +587,143 @@ export function putProductRiskCountryPreference(countryCodes: string[]) { user_id: getCurrentUserId(), country_codes: countryCodes, }), - ) + ); } export function matchProductRiskShops(shopNames: string[]) { return unwrapJavaResponse( - post, { user_id: number; shop_names: string[] }>( - `${JAVA_API_PREFIX}/product-risk-resolve/match-shops`, - { user_id: getCurrentUserId(), shop_names: shopNames }, - ), - ) + post< + JavaApiResponse, + { user_id: number; shop_names: string[] } + >(`${JAVA_API_PREFIX}/product-risk-resolve/match-shops`, { + user_id: getCurrentUserId(), + shop_names: shopNames, + }), + ); } export interface ProductRiskDashboardVo { - candidateCount: number - processedTaskCount: number - successTaskCount: number - failedTaskCount: number + candidateCount: number; + processedTaskCount: number; + successTaskCount: number; + failedTaskCount: number; } export interface ProductRiskHistoryItem { - resultId?: number - taskId?: number - shopName?: string - shopId?: string - platform?: string - companyName?: string - matched?: boolean - matchStatus?: string - matchMessage?: string - taskStatus?: string - success?: boolean - error?: string - outputFilename?: string - downloadUrl?: string + resultId?: number; + taskId?: number; + shopName?: string; + shopId?: string; + platform?: string; + companyName?: string; + matched?: boolean; + matchStatus?: string; + matchMessage?: string; + taskStatus?: string; + success?: boolean; + error?: string; + outputFilename?: string; + downloadUrl?: string; } export interface ProductRiskHistoryVo { - items: ProductRiskHistoryItem[] + items: ProductRiskHistoryItem[]; } export interface ProductRiskTaskSummary { - id?: number - taskNo?: string - status?: string - errorMessage?: string - createdAt?: string - updatedAt?: string - finishedAt?: string + id?: number; + taskNo?: string; + status?: string; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; } export interface ProductRiskTaskDetailVo { - task?: ProductRiskTaskSummary - items?: ProductRiskHistoryItem[] + task?: ProductRiskTaskSummary; + items?: ProductRiskHistoryItem[]; } export interface ProductRiskTaskBatchVo { - items: ProductRiskTaskDetailVo[] - missingTaskIds?: number[] + items: ProductRiskTaskDetailVo[]; + missingTaskIds?: number[]; } export interface ProductRiskPendingDeleteVo { - removed: boolean + removed: boolean; } export interface ProductRiskCreateTaskVo { - taskId: number - items: ProductRiskHistoryItem[] + taskId: number; + items: ProductRiskHistoryItem[]; } export function getProductRiskDashboard() { return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/product-risk-resolve/dashboard`, { - params: { user_id: getCurrentUserId() }, - }), - ) + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/dashboard`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function getProductRiskHistory() { return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/product-risk-resolve/history`, { - params: { user_id: getCurrentUserId() }, - }), - ) + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/history`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function deleteProductRiskHistory(resultId: number) { return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/product-risk-resolve/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - }), - ) + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function createProductRiskTask(items: ProductRiskShopQueueItem[]) { return unwrapJavaResponse( - post, { user_id: number; items: ProductRiskShopQueueItem[] }>( - `${JAVA_API_PREFIX}/product-risk-resolve/tasks`, - { user_id: getCurrentUserId(), items }, - ), - ) + post< + JavaApiResponse, + { user_id: number; items: ProductRiskShopQueueItem[] } + >(`${JAVA_API_PREFIX}/product-risk-resolve/tasks`, { + user_id: getCurrentUserId(), + items, + }), + ); } export function deleteProductRiskTask(taskId: number) { return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/product-risk-resolve/tasks/${taskId}`, { - params: { user_id: getCurrentUserId() }, - }), - ) + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/tasks/${taskId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); } export function deletePendingProductRiskShopResult(shopName: string) { return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/product-risk-resolve/pending-shop-result`, { - params: { user_id: getCurrentUserId(), shop_name: shopName }, - }), - ) + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/pending-shop-result`, + { + params: { user_id: getCurrentUserId(), shop_name: shopName }, + }, + ), + ); } export function getProductRiskTasksBatch(taskIds: number[]) { @@ -591,21 +732,30 @@ export function getProductRiskTasksBatch(taskIds: number[]) { `${JAVA_API_PREFIX}/product-risk-resolve/tasks/batch`, { taskIds }, ), - ) + ); } export function getProductRiskResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/product-risk-resolve/results/${resultId}/download`) + return getJavaDownloadUrl( + `/product-risk-resolve/results/${resultId}/download`, + ); } export function getJavaDownloadUrl(path: string) { - let raw = path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}` - // 核心修复:pywebview 的 save_file_from_url 需要完整带 schema 的 URL - if (!raw.startsWith('http://') && !raw.startsWith('https://') && typeof window !== 'undefined') { - raw = `${window.location.origin}${raw}` + let raw = + path.startsWith("http://") || path.startsWith("https://") + ? path + : `${JAVA_API_PREFIX}${path}`; + // 核心修复:pywebview 的 save_file_from_url_new 需要完整带 schema 的 URL + if ( + !raw.startsWith("http://") && + !raw.startsWith("https://") && + typeof window !== "undefined" + ) { + raw = `${window.location.origin}${raw}`; } - const separator = raw.includes('?') ? '&' : '?' - return `${raw}${separator}user_id=${encodeURIComponent(String(getCurrentUserId()))}` + const separator = raw.includes("?") ? "&" : "?"; + return `${raw}${separator}user_id=${encodeURIComponent(String(getCurrentUserId()))}`; } -export { JAVA_API_PREFIX, http } +export { JAVA_API_PREFIX, http }; diff --git a/frontend-vue/src/shared/bridges/pywebview.ts b/frontend-vue/src/shared/bridges/pywebview.ts index 2a08292..4bc2ffe 100644 --- a/frontend-vue/src/shared/bridges/pywebview.ts +++ b/frontend-vue/src/shared/bridges/pywebview.ts @@ -1,58 +1,91 @@ export interface UploadedJavaFile { - fileKey: string - originalFilename: string - localPath: string - size: number - relativePath?: string + fileKey: string; + originalFilename: string; + localPath: string; + size: number; + relativePath?: string; } export interface PywebviewApi { - close?: () => Promise - minimize?: () => Promise - maximize?: () => Promise - toggle_maximize?: () => Promise - save_image?: (urlOrData: string, filename?: string) => Promise<{ success: boolean; path?: string; error?: string }> - save_image_to_folder?: (urlOrData: string, dirPath: string, filename?: string) => Promise<{ success: boolean; path?: string; error?: string }> - select_folder?: () => Promise - select_brand_xlsx_files?: () => Promise - select_brand_folder?: () => Promise - upload_file_to_java?: (filePath: string, relativePath?: string) => Promise<{ success: boolean; message?: string; data?: UploadedJavaFile; error?: string }> - save_file_from_url?: (url: string, filename: string) => Promise<{ success: boolean; path?: string; error?: string }> - save_template_xlsx?: () => Promise<{ success: boolean; path?: string; error?: string }> - save_template_zip?: () => Promise<{ success: boolean; path?: string; error?: string }> - enqueue_json?: (data: unknown) => Promise<{ success: boolean; queue_size?: number; error?: string }> + close?: () => Promise; + minimize?: () => Promise; + maximize?: () => Promise; + toggle_maximize?: () => Promise; + save_image?: ( + urlOrData: string, + filename?: string, + ) => Promise<{ success: boolean; path?: string; error?: string }>; + save_image_to_folder?: ( + urlOrData: string, + dirPath: string, + filename?: string, + ) => Promise<{ success: boolean; path?: string; error?: string }>; + select_folder?: () => Promise; + select_brand_xlsx_files?: () => Promise; + select_brand_folder?: () => Promise; + upload_file_to_java?: ( + filePath: string, + relativePath?: string, + ) => Promise<{ + success: boolean; + message?: string; + data?: UploadedJavaFile; + error?: string; + }>; + save_file_from_url_new?: ( + url: string, + filename: string, + ) => Promise<{ success: boolean; path?: string; error?: string }>; + save_template_xlsx?: () => Promise<{ + success: boolean; + path?: string; + error?: string; + }>; + save_template_zip?: () => Promise<{ + success: boolean; + path?: string; + error?: string; + }>; + enqueue_json?: ( + data: unknown, + ) => Promise<{ success: boolean; queue_size?: number; error?: string }>; } declare global { interface Window { pywebview?: { - api?: PywebviewApi - } + api?: PywebviewApi; + }; } } -let cachedPywebviewApi: PywebviewApi | undefined -let pywebviewReadyBound = false +let cachedPywebviewApi: PywebviewApi | undefined; +let pywebviewReadyBound = false; function syncPywebviewApi() { - cachedPywebviewApi = window.pywebview?.api - return cachedPywebviewApi + cachedPywebviewApi = window.pywebview?.api; + return cachedPywebviewApi; } function bindPywebviewReady() { - if (pywebviewReadyBound || typeof window === 'undefined' || !window.addEventListener) return - pywebviewReadyBound = true - window.addEventListener('pywebviewready', () => { - syncPywebviewApi() - }) + if ( + pywebviewReadyBound || + typeof window === "undefined" || + !window.addEventListener + ) + return; + pywebviewReadyBound = true; + window.addEventListener("pywebviewready", () => { + syncPywebviewApi(); + }); } -bindPywebviewReady() +bindPywebviewReady(); export function getPywebviewApi() { - return syncPywebviewApi() || cachedPywebviewApi + return syncPywebviewApi() || cachedPywebviewApi; } export function hasPywebview() { - return Boolean(getPywebviewApi()) + return Boolean(getPywebviewApi()); } diff --git a/source_code/main.py b/source_code/main.py index eb64fdf..634a142 100644 --- a/source_code/main.py +++ b/source_code/main.py @@ -143,7 +143,7 @@ class WindowAPI: ) return result[0] if result else '' - def save_file_from_url(self, url, default_filename='download.zip'): + def save_file_from_url_new(self, url, default_filename='download.zip'): """弹窗选择保存位置,从 url 下载文件并保存。用于品牌任务结果 zip 等。""" if not url or not url.strip(): return {'success': False, 'error': '下载地址为空'} @@ -332,7 +332,7 @@ def main(): easy_drag=True ) api = WindowAPI(window) - window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.save_file_from_url, api.save_template_xlsx,api.save_template_zip) + window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.save_file_from_url_new, api.save_template_xlsx,api.save_template_zip) webview.start( debug=True, storage_path=cache_path, diff --git a/web_source/templates_backup/brand.html b/web_source/templates_backup/brand.html index 81aa193..842827a 100644 --- a/web_source/templates_backup/brand.html +++ b/web_source/templates_backup/brand.html @@ -806,7 +806,7 @@ } var zipUrl = task.result_paths.zip_url; var filename = 'brand_task_' + taskId + '.zip'; - if (api && api.save_file_from_url) { + if (api && api.save_file_from_url_new) { a.style.pointerEvents = 'none'; var done = function(ret) { if (ret && ret.success) { @@ -817,7 +817,7 @@ a.style.pointerEvents = ''; }; try { - var ret = api.save_file_from_url(zipUrl, filename); + var ret = api.save_file_from_url_new(zipUrl, filename); if (ret && typeof ret.then === 'function') { ret.then(done).catch(function(err) { showToast('保存失败:' + (err && err.message || err)); a.style.pointerEvents = ''; }); } else {