aliChoice 填到最后一步(差「请选择物流属性」);修 fillRequiredSelects 三个真 bug(attr-label.required 不认 / 容器项误点 / 下拉读残留)

This commit is contained in:
admin9webs committed 2026-09-20 21:24:40 +08:00
1 parent acabadffd4
commit 924ca86fdc
6 files changed
+254 -40

No files matched your search

+1 -1
View File
@@ -75,7 +75,7 @@ function wrap(o,n,l){const f=o[n];o[n]=function(){window.__cap.push(l+': '+[...a
| 平台 | add 路由 | 店铺控件 | 类目控件 | store 家族 |
|---|---|---|---|---|
| Joom | `/web/joomProduct/add` | **复选框组** 85 个(全部+84 店) | 可搜索 `.ant-select` 平铺列表 | `smtChoice*` + `joom*`(已跑通) |
| aliChoice | `/web/smtChoice/add` | 下拉(实测 2 家:XYZ Store / UniStyle Bag Store) | 弹窗 + 自动识别分类 | `smtChoice*`,**但与 Joom 字段名不同**:标题在 `smtChoiceAddBasicStore.subject`(Joom 是 `.name`);图片在 `smtChoiceAddProductStore.mainImage[]`。侦察档见 `alichoice建品/RECIPE.md` |
| aliChoice | `/web/smtChoice/add` | 下拉(实测 2 家:XYZ Store / UniStyle Bag Store) | 弹窗 + 自动识别分类 | `smtChoice*`,**字段名与 Joom 不同**:标题在 `smtChoiceAddBasicStore.subject`(Joom 是 `.name`);描述在 `smtChoiceAddStore.describeInfoData.detail`。**已填到最后一步**,卡在「**请选择物流属性**」(无 label 的报错块 + `icon_edit2` 编辑入口,对应 SKU 行的 `logisticAttrListJson`)——下一轮从这里接着做,细节见 `alichoice建品/RECIPE.md` |
| Lazada | `/web/lazada/add` | 下拉 | 弹窗 | 选完店铺才注册 `lazada*` store(懒加载) |
| fruugo | `/web/fruugoProduct/add` | 下拉 | 弹窗 | `fruugoAdd*`(Common/Basic/Dxm/Product/Variation/Image/Describe/Form) |
+37 -33
View File
@@ -1,41 +1,45 @@
# aliChoice(速卖通全托管)侦察 — 未跑通,下一轮从这里接着做
# aliChoice(速卖通全托管)配方 — 差最后一步,下一轮从「物流属性」接着做
路由:`https://www.dianxiaomi.com/web/smtChoice/add` 列表:`/web/smtChoice/{draft=采集箱, offline=待发布}`
页面标题:`店小秘--添加速卖通全托管产品`
## 已勘明(`alc_probe1.mjs`)
## 已经打通的(`alc_try1.mjs`,一条完整填到最后一步)
1. **店铺**:`店铺名称` 下拉(实测 2 家:`XYZ Store` / `UniStyle Bag Store`),取第一家 → `shopId=4393191`
2. **备货类型**:下拉 `国内履约` / `海外备货` → 取第一个(→ `productType='1'`)
3. **类目**:`选择分类` 弹窗 → 用 `sh_build.selectCategory(c, cat_kw, cat_kw)`(与 SHEIN/Temu 同一套)
4. **标题** → `smtChoiceAddBasicStore.formState.subject`(⚠️ **不是 Joom 的 `name`**)
5. **主图** → `smtChoiceAddProductStore.formState.mainImage[]`(吃 `{url,imgUrl,name,width,height}`)
6. **PC端描述** → `smtChoiceAddStore.describeInfoData.detail`(DOM 里是个 `TEXTAREA`)
7. **SKU 行** → `smtChoiceSkuDataStore.formState.skuDataList[]`
字段:`skuPrice / skuCode / scItemBarCode / packageWeight / packageLength / packageWidth /
packageHeight / logisticAttrListJson / logisticAttrList / imageList`
8. **必填类目属性**(品牌 / 高关注化学品 / 产地)→ `fillRequiredSelects()` 现已能自动填
(产地会输「中国」过滤后选 `中国大陆`)
### 店铺
`店铺名称` 是**下拉**,本例只有 **2 家**:`XYZ Store`、`UniStyle Bag Store`。
(上一轮侦察写的「64 家」是平台维度统计,实际可用店铺以后续页面为准。)
## ⛔ 当前唯一卡点:「请选择物流属性」
保存时钩子给 `MYJ.error: 校验失败`,DOM 里是一个**没有 label** 的报错块:
### 表单(可见,前 8 项)
`店铺名称 · 备货类型 · 产品标题 · 产品分类(+选择分类/自动识别分类) · 店小秘分类 · 来源URL · 产品属性`
```html
<div class="ant-form-item-has-error">
<span></span><i class="iconfont icon_edit2 link ml-4"></i>
<div class="text-red-500 font-size-12 mr-[20px]">请选择物流属性</div>
</div>
```
### store 与字段落点(**注意:和 Joom 共用 `smtChoice*`,但字段名不同**)
| store | 关键字段 |
|---|---|
| `smtChoiceAddBasicStore` | `shopId`、`categoryId`、`productType`、**`subject`(产品标题在这里,不是 `name`)** |
| `smtChoiceAddProductStore` | `mainImage[]`(数组)、`whiteBgImg`、`sceneImg`、`productManual`、`installVideo`、`videoData` |
| `smtChoiceSkuDataStore` | `skuDataList[]`(默认已生成 1 行) |
| `smtChoiceSkuAttrStore` | `skuAttrs[]` |
| `smtChoiceAddAttrStore` | `customAttrs[]` + 一大票汽配字段(`motorIds` / `vehicleEuAfids` …) |
| `smtChoiceAddOtherStore` | `packageFormState` / `otherInfoFormState` / `otherBusinessInfoState` |
| `smtChoiceAddStore` | `productInfo` / `attrsInfo` / `describeInfoData`(**没有 formState**) |
- 它带一个 `icon_edit2 link` 的编辑入口 → 点开大概率是「物流属性」弹窗/抽屉
- SKU 行里有 `logisticAttrListJson` / `logisticAttrList` 两个字段空着,应该就是它要填的
- 下一步:点那个 `icon_edit2`,dump 弹窗结构(很可能要按类目选物流渠道/时效)
⚠️ 和 Joom 的差异:Joom 标题在 `smtChoiceAddBasicStore.name`,这里在 `.subject`。
**别把 Joom 的填法直接抄过来。**
## 另一个未验证项:4 个规格组都是 0 选项
`smtChoiceSkuAttrStore.formState.skuAttrs` 有 4 组:
`Ships From(200007763) / Color(14) / Number of Pcs(200116262) / Bulk Buy(200483263)`,
但 `options` 都是 0(值在 `_options` 或要异步加载),DOM 里能看到值(卡其色/粉红/6Pc/Set…)。
**未验证变种值是否必须选**——先解物流属性,再看保存是否又冒出新的一层。
## 下一轮的顺序建议
1. 选店铺(下拉,取第一家 `XYZ Store`)
2. **备货类型**(必填,先探选项)
3. 类目:`选择分类` 弹窗(同 SHEIN/Temu 那套)+ 留意「自动识别分类」
4. 标题 → `smtChoiceAddBasicStore.subject`
5. 图片 → `smtChoiceAddProductStore.mainImage[]`
6. SKU 行 / 属性 / 包装 → 对应 store
7. **挂钩子再保存**(大概率还有层层递进的必填:参考 Temu 的省份→变种→尺寸→净重→预览图)
## 已知可复用的东西
- `shein建品/sh_lib.mjs`(`newPage`/`closePage`/`installHook`/`readHook`)
- `shein建品/sh_build.mjs` 的 `selectCategory` / `fillRequiredSelects`(SHEIN/Temu/Joom 都已验证)
- `joom建品/jmbatch.mjs` 的骨架(换字段名和控件类型即可)
## 本轮顺手修好的共享 bug(`shein建品/sh_build.mjs` 的 `fillRequiredSelects`)
之前它在 aliChoice 上**一个必填属性都填不上**,三个原因,都已修:
1. select 分支的必填判断只看 `label.ant-form-item-required`,而速卖通系的属性是
`.attr-label.required`(label 上还常带 `*` 前缀)→ 现在三种标记都认
2. 容器项「产品属性」本身也带 `*`,会被当成一个字段去点 → 现在跳过**含嵌套 `.ant-form-item`** 的项
3. 点开下拉后按 `.pop()` 取最后一个可见下拉,容易读到**上一个字段残留**的列表
(曾把化学物列表当国家列表)→ 现在①每次点前先 `Esc` ②按**与点击点的距离**挑下拉
③产地/Origin 先 `Input.insertText('中国')` 过滤
+66
View File
@@ -0,0 +1,66 @@
import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { selectCategory } from '/home/admin9webs/shein建品/sh_build.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
import { readFileSync } from 'fs';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const pid = process.argv[2] || '676236596821';
const it = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json','utf8'))[pid];
const c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add');
await wait(12000);
// 1) 店铺下拉
const shop = await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const s=f?f.querySelector('.ant-select-selector'):null; if(!s) return 'no-sel'; s.scrollIntoView({block:'center'});
const r=s.getBoundingClientRect(); return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`);
const sp = JSON.parse(String(shop));
if (sp.length){
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:sp[0],y:sp[1],buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:sp[0],y:sp[1],button:'left',clickCount:1,buttons:1});
await wait(2000);
}
console.log('选店:', String(await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd'; const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; if(!op) return 'no-op';
const r=op.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) op.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2}));
return (op.innerText||'').trim();})()`)));
await wait(6000);
// 2) 备货类型
const bp = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/备货类型/.test((e.querySelector('label')||{}).innerText||''));
if(!f) return '[]';
const s=f.querySelector('.ant-select-selector');
const ctrls=[...f.querySelectorAll('.ant-select,.ant-radio-group,.ant-btn-group')].map(x=>x.tagName+'.'+String(x.className).slice(0,26));
if(!s) return JSON.stringify({ctrls, txt:(f.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60)});
s.scrollIntoView({block:'center'}); const r=s.getBoundingClientRect();
return JSON.stringify({ctrls, txt:(f.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60), x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)});})()`)));
console.log('备货类型控件:', JSON.stringify(bp).slice(0,300));
if (bp.x){
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:bp.x,y:bp.y,buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:bp.x,y:bp.y,button:'left',clickCount:1,buttons:1});
await wait(2200);
console.log('备货类型选项:', S(String(await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd'; const items=[...dd.querySelectorAll('.ant-select-item-option')].map(e=>(e.innerText||'').trim());
return JSON.stringify(items.slice(0,8));})()`))).slice(0,300));
await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; if(!op) return 0; const r=op.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) op.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2})); return 1;})()`);
await wait(3000);
}
// 3) 类目
console.log('类目:', S(String(await selectCategory(c, it.cat_kw || '发夹', it.cat_kw))).slice(0,160));
await wait(5000);
// 4) 表单项 + store
const items = await c.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).slice(0,20).map(e=>{
const l=e.querySelector('label'); const req=l?(/required/.test(String(l.className))||/required/.test(String((e.querySelector('.attr-label')||{}).className||''))?'*':''):'';
return req+((e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,44));}))`);
console.log('表单项:', S(String(items)).slice(0,1200));
const st = await c.evalJS(`(()=>{const p=${P};
const ids=(p._s instanceof Map?[...p._s.keys()]:Object.keys(p._s)).filter(x=>/smtChoice/i.test(x));
const out={};
const fmt=(v,d=0)=>{ if(v===null||v===undefined) return String(v); if(Array.isArray(v)) return '['+v.length+']'+(v[0]?JSON.stringify(v[0]).slice(0,70):'');
if(typeof v==='object'){ if(d>1) return '{...}'; const o={}; let n=0; for(const k of Object.keys(v)){o[k]=fmt(v[k],d+1); if(++n>14){o['…']='';break;}} return o;} return String(v).slice(0,40); };
for(const id of ids){ try{ const s=(p._s.get?p._s.get(id):p._s[id]).$state; out[id]= s.formState?fmt(s.formState):('keys='+Object.keys(s).slice(0,8).join(',')); }catch(e){out[id]='ERR';} }
return JSON.stringify(out);})()`);
console.log('stores:', JSON.stringify(JSON.parse(String(st)),null,1).slice(0,1800));
try{ await c.shot('/tmp/alc2.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+42
View File
@@ -0,0 +1,42 @@
import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { selectCategory } from '/home/admin9webs/shein建品/sh_build.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
import { readFileSync } from 'fs';
const pid='676236596821';
const it = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json','utf8'))[pid];
const c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add');
await wait(12000);
// 店 + 备货类型(照抄 try1 的两步)
for (const [label, hint] of [['店铺名称','店铺'],['备货类型','备货']]) {
const p = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>new RegExp(${JSON.stringify(label)}).test((e.querySelector('label')||{}).innerText||''));
const s=f?f.querySelector('.ant-select-selector'):null; if(!s) return '[]'; s.scrollIntoView({block:'center'});
const r=s.getBoundingClientRect(); return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`)));
if(!p.length) { console.log(hint,'没找到'); continue; }
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:p[0],y:p[1],buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:p[0],y:p[1],button:'left',clickCount:1,buttons:1});
await wait(2000);
console.log(hint,'选中:', String(await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd'; const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; if(!op) return 'no-op';
const r=op.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) op.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2}));
return (op.innerText||'').trim();})()`)));
await wait(label==='店铺名称'?6000:3000);
}
console.log('类目:', S(String(await selectCategory(c, it.cat_kw||'发夹', it.cat_kw))).slice(0,90));
await wait(6000);
const d = await c.evalJS(`(()=>{
const want=['品牌','高关注化学品','产地'];
const out=[];
for (const e of document.querySelectorAll('.ant-form-item')) {
if (e.offsetParent===null) continue;
const l=e.querySelector('label'); const t=l?(l.innerText||'').replace(/\\s+/g,' ').trim():'';
if(!want.some(w=>t.startsWith(w)||t.startsWith('*'+w))) continue;
const sel=e.querySelector('.ant-select');
out.push({ txt:t.slice(0,30), labelCls:l?String(l.className).slice(0,60):'', labelHTML:l?l.outerHTML.replace(/\\s+/g,' ').slice(0,150):'',
hasAttrLabel:!!e.querySelector('.attr-label'), attrCls:String((e.querySelector('.attr-label')||{}).className||''),
selTxt: sel?(sel.innerText||'').replace(/\\s+/g,' ').trim().slice(0,20):'no-sel',
nested: !!e.parentElement.closest('.ant-form-item') });
}
return JSON.stringify(out);})()`);
console.log('必填属性项:', JSON.stringify(JSON.parse(String(d)), null, 1).slice(0,2000));
await closePage(c); process.exit(0);
+88
View File
@@ -0,0 +1,88 @@
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { selectCategory, fillRequiredSelects } from '/home/admin9webs/shein建品/sh_build.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
import { readFileSync } from 'fs';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const pid = process.argv[2] || '676236596821';
const it = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json','utf8'))[pid];
const IMGS = (it.imgs||[]).slice(0,6);
const c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add');
await wait(12000); await installHook(c);
// 店
const sp = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const s=f.querySelector('.ant-select-selector'); s.scrollIntoView({block:'center'}); const r=s.getBoundingClientRect();
return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`)));
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:sp[0],y:sp[1],buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:sp[0],y:sp[1],button:'left',clickCount:1,buttons:1});
await wait(2000);
await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; const r=op.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) op.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2})); return 1;})()`);
await wait(6000);
// 备货类型
const bp = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/备货类型/.test((e.querySelector('label')||{}).innerText||''));
const s=f.querySelector('.ant-select-selector'); s.scrollIntoView({block:'center'}); const r=s.getBoundingClientRect();
return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`)));
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:bp[0],y:bp[1],buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:bp[0],y:bp[1],button:'left',clickCount:1,buttons:1});
await wait(2000);
await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; const r=op.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) op.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2})); return 1;})()`);
await wait(3000);
console.log('类目:', S(String(await selectCategory(c, it.cat_kw || '发夹', it.cat_kw))).slice(0,120));
await wait(6000);
// 结构 dump
const shape = await c.evalJS(`(()=>{const p=${P};
const sd=p._s.get('smtChoiceSkuDataStore').$state.formState;
const sku=JSON.stringify(sd.skuDataList&&sd.skuDataList[0]||{}).slice(0,700);
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
const sa=p._s.get('smtChoiceSkuAttrStore').$state.formState.skuAttrs||[];
return JSON.stringify({skuRow:sku, descKeys:Object.keys(desc||{}).slice(0,10), desc:JSON.stringify(desc||{}).slice(0,300),
attrs:sa.map(a=>a.attributeId+':'+a.attributeName+' opts='+((a.options||a._options||[]).length))});})()`);
console.log('结构:', S(String(shape)).slice(0,1200));
// PC端描述 DOM
const dom = await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/PC端描述/.test((e.querySelector('label')||{}).innerText||''));
if(!f) return 'no-item';
return JSON.stringify({tags:[...f.querySelectorAll('input,textarea,[contenteditable],.ql-editor,.w-e-text')].map(x=>x.tagName+(x.className?'.'+String(x.className).slice(0,30):'')).slice(0,6),
txt:(f.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60)});})()`);
console.log('PC端描述:', S(String(dom)).slice(0,400));
// 填 store
const fill = await c.evalJS(`(()=>{const p=${P}; const imgs=${JSON.stringify(IMGS)};
const mk=(u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800});
const b=p._s.get('smtChoiceAddBasicStore').$state.formState;
b.subject=${JSON.stringify(it.title)};
const pr=p._s.get('smtChoiceAddProductStore').$state.formState;
pr.mainImage=imgs.map(mk);
const sd=p._s.get('smtChoiceSkuDataStore').$state.formState;
(sd.skuDataList||[]).forEach((r,i)=>{ for(const k of ['skuPrice','price','skuStock','stock','inventory','skuSupplierCode','supplierCode','skuCode','code']) { if(k in r) { if(/price/i.test(k)) r[k]=String(${JSON.stringify(String(it.price_cny||'19.90'))}); else if(/stock|inventory/i.test(k)) r[k]='999'; else r[k]='AC-'+${JSON.stringify(pid)}+'-'+(i+1); } } });
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
if(desc) desc.detail=(desc.detail||'')+${JSON.stringify((it.bullets||[]).join('<br/>'))};
(sd.skuDataList||[]).forEach(r=>{ r.skuPrice=String(${JSON.stringify(String(it.price_cny||'19.90'))}); r.skuCode='AC-'+${JSON.stringify(pid)};
r.packageWeight='100'; r.packageLength='20'; r.packageWidth='15'; r.packageHeight='3'; });
return JSON.stringify({subject:b.subject.slice(0,20), imgs:pr.mainImage.length, rows:(sd.skuDataList||[]).length, desc:(desc&&desc.detail||'').length});})()`);
console.log('填充:', S(String(fill)).slice(0,300));
await wait(3500);
console.log('必填属性:', S(String(await fillRequiredSelects(c))).slice(0,300));
await wait(2000);
// 保存
await c.evalJS(`window.scrollTo(0,0)`); await wait(600);
await c.evalJS(`(()=>{const xs=[...document.querySelectorAll('button')].filter(e=>e.offsetParent!==null);
const b=xs.find(e=>/^\\s*保存\\s*$/.test((e.innerText||'').trim()))||xs.find(e=>/保存/.test(e.innerText||''));
if(b) for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 1;})()`);
await wait(14000);
const detail = await c.evalJS(`(()=>{const bad=[...document.querySelectorAll('.ant-form-item-has-error')].map(x=>({
label:(x.querySelector('label')||{}).innerText||'(无)',
cls:String(x.className).slice(0,50),
html:x.outerHTML.replace(/\s+/g,' ').slice(0,420)}));
const sa=(()=>{const p=${P};return (p._s.get('smtChoiceSkuAttrStore').$state.formState.skuAttrs||[]).map(a=>({id:a.attributeId,name:a.attributeName,opts:(a.options||[]).length,sel:(a.options||[]).filter(o=>o.selected||o.checked).length}));})();
const sd=(()=>{const p=${P};const r=(p._s.get('smtChoiceSkuDataStore').$state.formState.skuDataList||[])[0]||{};return {keys:Object.keys(r),skuPrice:r.skuPrice,skuCode:r.skuCode,w:r.packageWeight};})();
return JSON.stringify({bad, sa, sd});})()`);
console.log('错误详情:', S(String(detail)).slice(0,1100));
const res = await c.evalJS(`JSON.stringify({
mods:[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,120)),
errItems:[...document.querySelectorAll('.ant-form-item-has-error')].map(x=>{const l=x.querySelector('label'); return l?(l.innerText||'').replace(/\\s+/g,' ').trim().slice(0,26):'?';}).slice(0,8)})`);
console.log('结果:', S(String(res)).slice(0,600));
console.log('hook:', String(await readHook(c)).slice(-320));
try{ await c.shot('/tmp/alc_try1.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+20 -6
View File
@@ -202,7 +202,9 @@ export async function fillRequiredSelects(c, skip = ['店铺账号', '产品分
const r = await c.evalJS(`(()=>{
const items=[...document.querySelectorAll('.ant-form-item')].filter(e=>{
if(e.offsetParent===null) return false;
const req=e.querySelector('.attr-label.required')||e.querySelector('label.ant-form-item-required');
const lab=e.querySelector('label');
const req=e.querySelector('.attr-label.required')||(lab&&/required/.test(String(lab.className))?lab:null)
||(lab&&(lab.innerText||'').trim().startsWith('*')?lab:null);
if(!req) return false;
const name=(req.innerText||'').replace(/\s+/g,' ').trim();
if(${JSON.stringify(skip)}.some(s=>name.includes(s))) return false;
@@ -229,8 +231,11 @@ export async function fillRequiredSelects(c, skip = ['店铺账号', '产品分
for (let round = 0; round < 8; round++) {
const tgt = JSON.parse(String(await c.evalJS(`(()=>{
const items=[...document.querySelectorAll('.ant-form-item')].filter(e=>{
const l=e.querySelector('label'); if(!l||!/ant-form-item-required/.test(String(l.className))) return false;
const l=e.querySelector('label');
const reqOk = l && (/required/.test(String(l.className)) || (l.innerText||'').trim().startsWith('*') || e.querySelector('.attr-label.required'));
if(!reqOk) return false;
if(e.offsetParent===null) return false;
if(e.querySelector('.ant-form-item')) return false; // 容器项(如「产品属性」):只管叶子项
const name=(l.innerText||'').replace(/\\s+/g,' ').trim();
if(${JSON.stringify(skip)}.some(s=>name.includes(s))) return false;
const sel=e.querySelector('.ant-select'); if(!sel) return false;
@@ -241,13 +246,22 @@ export async function fillRequiredSelects(c, skip = ['店铺账号', '产品分
sel.scrollIntoView({block:'center'}); const r=sel.getBoundingClientRect();
return JSON.stringify({name:(l.innerText||'').replace(/\\s+/g,' ').trim().slice(0,30), x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)});})()`)));
if (!tgt.name) break;
await c.send('Input.dispatchKeyEvent', {type:'keyDown', key:'Escape', code:'Escape', windowsVirtualKeyCode:27}).catch(()=>{});
await wait(400);
await c.send('Input.dispatchMouseEvent', {type:'mouseMoved', x:tgt.x, y:tgt.y, buttons:0}); await wait(250);
for (const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent', {type:t, x:tgt.x, y:tgt.y, button:'left', clickCount:1, buttons:1});
await wait(1800);
const pick = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd';
const op=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null)[0];
if(!op) return 'no-op';
if (/产地|Origin/i.test(tgt.name)) { await c.send('Input.insertText', {text:'中国'}).catch(()=>{}); await wait(1500); }
const pick = await c.evalJS(`(()=>{const cy=${JSON.stringify(tgt.y)};
const cands=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null);
if(!cands.length) return 'no-dd';
const dd=cands.map(e=>({e,d:Math.abs(e.getBoundingClientRect().top-cy)})).sort((a,b)=>a.d-b.d)[0].e;
const ops=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null);
if(!ops.length) return 'no-op';
const want=${JSON.stringify(tgt.name)};
const clean=x=>(x.innerText||'').replace(/\s+/g,' ').trim();
const op=(/产地|Origin/i.test(want) ? ops.find(o=>/中国|China/i.test(clean(o))) : null)
|| ops.find(o=>!/^(无|none|NONE)/.test(clean(o))) || ops[0];
const r=op.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) op.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2}));
return 'picked:'+(op.innerText||'').trim().slice(0,18);})()`);