aliChoice 跑通并开始批量:物流属性(保存两遍)+CKEditor 描述;配方与批量脚本入库
This commit is contained in:
1 parent
924ca86fdc
commit
0466698490
5 files changed
+363
-10
No files matched your search
@@ -75,7 +75,7 @@ function wrap(o,n,l){const f=o[n];o[n]=function(){window.__cap.push(l+': '+[...a
|
|||||||
| 平台 | add 路由 | 店铺控件 | 类目控件 | store 家族 |
|
| 平台 | add 路由 | 店铺控件 | 类目控件 | store 家族 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| Joom | `/web/joomProduct/add` | **复选框组** 85 个(全部+84 店) | 可搜索 `.ant-select` 平铺列表 | `smtChoice*` + `joom*`(已跑通) |
|
| 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`);描述在 `smtChoiceAddStore.describeInfoData.detail`。**已填到最后一步**,卡在「**请选择物流属性**」(无 label 的报错块 + `icon_edit2` 编辑入口,对应 SKU 行的 `logisticAttrListJson`)——下一轮从这里接着做,细节见 `alichoice建品/RECIPE.md` |
|
| aliChoice | `/web/smtChoice/add` | 下拉(实测 2 家) | 弹窗 | `smtChoice*`:标题 `.subject`、描述要写 **CKEditor setData**、保存要**两遍**(第二遍前补「物流属性=普货」)——**已跑通**,`alcbatch.mjs` 批量中 |
|
||||||
| Lazada | `/web/lazada/add` | 下拉 | 弹窗 | 选完店铺才注册 `lazada*` store(懒加载) |
|
| Lazada | `/web/lazada/add` | 下拉 | 弹窗 | 选完店铺才注册 `lazada*` store(懒加载) |
|
||||||
| fruugo | `/web/fruugoProduct/add` | 下拉 | 弹窗 | `fruugoAdd*`(Common/Basic/Dxm/Product/Variation/Image/Describe/Form) |
|
| fruugo | `/web/fruugoProduct/add` | 下拉 | 弹窗 | `fruugoAdd*`(Common/Basic/Dxm/Product/Variation/Image/Describe/Form) |
|
||||||
|
|
||||||
|
|||||||
+28
-9
@@ -1,21 +1,21 @@
|
|||||||
# aliChoice(速卖通全托管)配方 — 差最后一步,下一轮从「物流属性」接着做
|
# aliChoice(速卖通全托管)配方 — **已跑通**(批量进行中)
|
||||||
|
|
||||||
路由:`https://www.dianxiaomi.com/web/smtChoice/add` 列表:`/web/smtChoice/{draft=采集箱, offline=待发布}`
|
路由:`https://www.dianxiaomi.com/web/smtChoice/add` 列表:`/web/smtChoice/{draft=采集箱, offline=待发布}`
|
||||||
|
|
||||||
## 已经打通的(`alc_try1.mjs`,一条完整填到最后一步)
|
## 已经打通的(批量脚本 `alcbatch.mjs`,单条已验证保存成功)
|
||||||
1. **店铺**:`店铺名称` 下拉(实测 2 家:`XYZ Store` / `UniStyle Bag Store`),取第一家 → `shopId=4393191`
|
1. **店铺**:`店铺名称` 下拉(实测 2 家:`XYZ Store` / `UniStyle Bag Store`),取第一家 → `shopId=4393191`
|
||||||
2. **备货类型**:下拉 `国内履约` / `海外备货` → 取第一个(→ `productType='1'`)
|
2. **备货类型**:下拉 `国内履约` / `海外备货` → 取第一个(→ `productType='1'`)
|
||||||
3. **类目**:`选择分类` 弹窗 → 用 `sh_build.selectCategory(c, cat_kw, cat_kw)`(与 SHEIN/Temu 同一套)
|
3. **类目**:`选择分类` 弹窗 → 用 `sh_build.selectCategory(c, cat_kw, cat_kw)`(与 SHEIN/Temu 同一套)
|
||||||
4. **标题** → `smtChoiceAddBasicStore.formState.subject`(⚠️ **不是 Joom 的 `name`**)
|
4. **标题** → `smtChoiceAddBasicStore.formState.subject`(⚠️ **不是 Joom 的 `name`**)
|
||||||
5. **主图** → `smtChoiceAddProductStore.formState.mainImage[]`(吃 `{url,imgUrl,name,width,height}`)
|
5. **主图** → `smtChoiceAddProductStore.formState.mainImage[]`(吃 `{url,imgUrl,name,width,height}`)
|
||||||
6. **PC端描述** → `smtChoiceAddStore.describeInfoData.detail`(DOM 里是个 `TEXTAREA`)
|
6. **PC端描述** → `smtChoiceAddStore.describeInfoData.detail` + **CKEditor setData**(见下)
|
||||||
7. **SKU 行** → `smtChoiceSkuDataStore.formState.skuDataList[]`
|
7. **SKU 行** → `smtChoiceSkuDataStore.formState.skuDataList[]`
|
||||||
字段:`skuPrice / skuCode / scItemBarCode / packageWeight / packageLength / packageWidth /
|
字段:`skuPrice / skuCode / scItemBarCode / packageWeight / packageLength / packageWidth /
|
||||||
packageHeight / logisticAttrListJson / logisticAttrList / imageList`
|
packageHeight / logisticAttrListJson / logisticAttrList / imageList`
|
||||||
8. **必填类目属性**(品牌 / 高关注化学品 / 产地)→ `fillRequiredSelects()` 现已能自动填
|
8. **必填类目属性**(品牌 / 高关注化学品 / 产地)→ `fillRequiredSelects()` 现已能自动填
|
||||||
(产地会输「中国」过滤后选 `中国大陆`)
|
(产地会输「中国」过滤后选 `中国大陆`)
|
||||||
|
|
||||||
## ⛔ 当前唯一卡点:「请选择物流属性」
|
## ✅ 「请选择物流属性」已解(保存两遍)
|
||||||
保存时钩子给 `MYJ.error: 校验失败`,DOM 里是一个**没有 label** 的报错块:
|
保存时钩子给 `MYJ.error: 校验失败`,DOM 里是一个**没有 label** 的报错块:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
@@ -25,15 +25,34 @@
|
|||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
- 它带一个 `icon_edit2 link` 的编辑入口 → 点开大概率是「物流属性」弹窗/抽屉
|
解法(**保存两遍**):
|
||||||
- SKU 行里有 `logisticAttrListJson` / `logisticAttrList` 两个字段空着,应该就是它要填的
|
1. 先点一次保存 → 报错块出现
|
||||||
- 下一步:点那个 `icon_edit2`,dump 弹窗结构(很可能要按类目选物流渠道/时效)
|
2. 点报错块里的 `i.icon_edit2` → 弹出「修改物流属性」modal(20 个复选框,按大类分组:
|
||||||
|
禁运 / 普货 / 纯电 / 强磁 / 特货(液体·粉末·膏体·其他) / 特敏 / 内电(8 种电池) / 外电 / 弱磁)
|
||||||
|
3. 勾 **普货** → 点「确定」→ 再点保存 → `您的产品已保存到「待发布」`
|
||||||
|
|
||||||
## 另一个未验证项:4 个规格组都是 0 选项
|
对应 SKU 行的 `logisticAttrListJson` / `logisticAttrList` 两个字段。
|
||||||
|
|
||||||
|
## ✅ PC端描述是 CKEditor,写 textarea 没用
|
||||||
|
DOM 里那个 `TEXTAREA` 是 `display:none` 的藏匿字段,真实控件是 **CKEditor**
|
||||||
|
(aria-label「所见即所得编辑器, cked…」)。两种赋值方式,实测都写上才稳:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 1) 给编辑器实例 setData(本例 window.CKEDITOR.instances 有 2 个实例)
|
||||||
|
Object.values(window.CKEDITOR.instances).forEach(x => x.setData(html));
|
||||||
|
// 2) 同时写 store
|
||||||
|
pinia._s.get('smtChoiceAddStore').$state.describeInfoData.detail = html;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 另一个未验证项:4 个规格组都是 0 选项## 另一个未验证项:4 个规格组都是 0 选项
|
||||||
`smtChoiceSkuAttrStore.formState.skuAttrs` 有 4 组:
|
`smtChoiceSkuAttrStore.formState.skuAttrs` 有 4 组:
|
||||||
`Ships From(200007763) / Color(14) / Number of Pcs(200116262) / Bulk Buy(200483263)`,
|
`Ships From(200007763) / Color(14) / Number of Pcs(200116262) / Bulk Buy(200483263)`,
|
||||||
但 `options` 都是 0(值在 `_options` 或要异步加载),DOM 里能看到值(卡其色/粉红/6Pc/Set…)。
|
但 `options` 都是 0(值在 `_options` 或要异步加载),DOM 里能看到值(卡其色/粉红/6Pc/Set…)。
|
||||||
**未验证变种值是否必须选**——先解物流属性,再看保存是否又冒出新的一层。
|
**实测:不选变种值也能保存成功**(保存链路里没有因为变种报错),所以批量里先不选。
|
||||||
|
|
||||||
|
## 单条顺序(批量脚本里的固定顺序)
|
||||||
|
`店铺 → 备货类型 → 类目 → 标题/主图/SKU行 → 必填属性(品牌/高关注化学品/产地) → PC端描述(CKEditor)
|
||||||
|
→ 保存① → 物流属性(普货) → 保存② → 待发布`
|
||||||
|
|
||||||
## 本轮顺手修好的共享 bug(`shein建品/sh_build.mjs` 的 `fillRequiredSelects`)
|
## 本轮顺手修好的共享 bug(`shein建品/sh_build.mjs` 的 `fillRequiredSelects`)
|
||||||
之前它在 aliChoice 上**一个必填属性都填不上**,三个原因,都已修:
|
之前它在 aliChoice 上**一个必填属性都填不上**,三个原因,都已修:
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
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(12000);
|
||||||
|
// 找报错块里的编辑入口
|
||||||
|
const info = await c.evalJS(`(()=>{
|
||||||
|
const bad=[...document.querySelectorAll('.ant-form-item-has-error')];
|
||||||
|
const out=bad.map(b=>({txt:(b.innerText||'').replace(/\\s+/g,' ').trim().slice(0,40), icons:[...b.querySelectorAll('i,a,.link')].map(x=>String(x.className).slice(0,40))}));
|
||||||
|
const link=document.querySelector('.ant-form-item-has-error .icon_edit2')||document.querySelector('.icon_edit2.link');
|
||||||
|
let pos='[]';
|
||||||
|
if(link){ link.scrollIntoView({block:'center'}); const r=link.getBoundingClientRect(); pos=JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]); }
|
||||||
|
return JSON.stringify({out, pos});})()`);
|
||||||
|
console.log('报错块:', S(String(info)).slice(0,500));
|
||||||
|
const pos = JSON.parse(String(JSON.parse(String(info)).pos || '[]'));
|
||||||
|
if(pos.length){
|
||||||
|
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:pos[0],y:pos[1],buttons:0}); await wait(300);
|
||||||
|
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:pos[0],y:pos[1],button:'left',clickCount:1,buttons:1});
|
||||||
|
await wait(4000);
|
||||||
|
}
|
||||||
|
const dlg = await c.evalJS(`(()=>{
|
||||||
|
const ds=[...document.querySelectorAll('.ant-modal,.ant-drawer')].filter(e=>e.offsetParent!==null)
|
||||||
|
.map(e=>({cls:String(e.className).slice(0,40), txt:(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,320),
|
||||||
|
checks:e.querySelectorAll('.ant-checkbox-wrapper,.ant-radio-wrapper').length, selects:e.querySelectorAll('.ant-select').length,
|
||||||
|
btns:[...e.querySelectorAll('button')].map(b=>(b.innerText||'').trim().slice(0,12)).filter(Boolean).slice(0,6)}));
|
||||||
|
return JSON.stringify({n:ds.length, ds:ds.slice(0,2)});})()`);
|
||||||
|
console.log('弹层:', S(String(dlg)).slice(0,1200));
|
||||||
|
try{ await c.shot('/tmp/alc_logi.jpg'); console.log('shot ok'); }catch(e){}
|
||||||
|
await closePage(c); process.exit(0);
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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);
|
||||||
|
// 只 dump PC端描述 控件属性
|
||||||
|
const d = 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';
|
||||||
|
const ta=f.querySelector('textarea');
|
||||||
|
const info=ta?{ro:ta.readOnly,dis:ta.disabled,cls:String(ta.className).slice(0,50),ph:ta.placeholder||'',rect:JSON.stringify(ta.getBoundingClientRect()),style:(ta.getAttribute('style')||'').slice(0,80)}:'no-ta';
|
||||||
|
const btns=[...f.querySelectorAll('button,a,span')].filter(x=>(x.innerText||'').trim()).map(x=>(x.innerText||'').trim().slice(0,14)).slice(0,8);
|
||||||
|
return JSON.stringify({info, btns, html:f.innerHTML.replace(/\\s+/g,' ').slice(0,600)});})()`);
|
||||||
|
console.log('PC端描述详情:', S(String(d)).slice(0,1200));
|
||||||
|
try{ await c.shot('/tmp/alc_desc.jpg'); console.log('shot ok'); }catch(e){}
|
||||||
|
await closePage(c); process.exit(0);
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* aliChoice(速卖通全托管)批量建品
|
||||||
|
*
|
||||||
|
* 用法: node alcbatch.mjs [--dry] [pid...]
|
||||||
|
*
|
||||||
|
* 顺序:店铺(下拉第一家) → 备货类型(国内履约) → 类目(弹窗) → 标题(subject)/主图/PC端描述
|
||||||
|
* → SKU 行 → 必填类目属性(品牌/高关注化学品/产地) → **物流属性(先保存一次触发报错,
|
||||||
|
* 点报错里的 icon_edit2 → 勾「普货」→ 确定)** → 再保存
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync, appendFileSync } from 'fs';
|
||||||
|
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
|
||||||
|
import { selectCategory, fillRequiredSelects } from '/home/admin9webs/shein建品/sh_build.mjs';
|
||||||
|
|
||||||
|
const DIR = '/home/admin9webs/alichoice建品';
|
||||||
|
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
|
||||||
|
const INPUT = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json', 'utf8'));
|
||||||
|
const DONE_F = `${DIR}/alc_done.json`;
|
||||||
|
const LOG_F = `${DIR}/alc_batch.log`;
|
||||||
|
const SKIP = new Set(['601964534137']); // 遥控器保护套:无干净图,全平台统一跳过
|
||||||
|
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
const dry = argv.includes('--dry');
|
||||||
|
const only = argv.filter(a => !a.startsWith('--'));
|
||||||
|
let tasks = Object.keys(INPUT).filter(pid => (INPUT[pid].imgs || []).length > 0).map(pid => ({ pid }));
|
||||||
|
if (only.length) tasks = tasks.filter(t => only.includes(t.pid)); else tasks = tasks.filter(t => !SKIP.has(t.pid));
|
||||||
|
let done = {}; try { done = JSON.parse(readFileSync(DONE_F, 'utf8')); } catch {}
|
||||||
|
const log = m => { const l = `[${new Date().toISOString().slice(11, 19)}] ${m}`; console.log(l); appendFileSync(LOG_F, l + '\n'); };
|
||||||
|
|
||||||
|
/** PC端描述:真实控件是 CKEditor(隐藏 textarea + 所见即所得),给编辑器实例 setData 才有效 */
|
||||||
|
async function fillDescription(c, text) {
|
||||||
|
return c.evalJS(`(()=>{const p=${P};
|
||||||
|
const html=${JSON.stringify(text)};
|
||||||
|
let how='none';
|
||||||
|
try{ if(window.CKEDITOR&&window.CKEDITOR.instances){ const inst=Object.values(window.CKEDITOR.instances);
|
||||||
|
if(inst.length){ inst.forEach(x=>{ try{ x.setData(html); }catch(e){} }); how='ckeditor:'+inst.length; } } }catch(e){}
|
||||||
|
const d=p._s.get('smtChoiceAddStore').$state.describeInfoData;
|
||||||
|
if(d){ d.detail=html; if('mobileDetail' in d) d.mobileDetail=html; how+='|store'; }
|
||||||
|
if(how==='none'){ const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null)
|
||||||
|
.find(e=>/PC端描述/.test((e.querySelector('label')||{}).innerText||''));
|
||||||
|
const ta=f?f.querySelector('textarea'):null;
|
||||||
|
if(ta){ const setter=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value').set;
|
||||||
|
setter.call(ta, html); ta.dispatchEvent(new Event('input',{bubbles:true})); how='textarea'; } }
|
||||||
|
return how;})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickFirstOption(c, labelRe) {
|
||||||
|
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(labelRe)}).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) return 'no-select';
|
||||||
|
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);
|
||||||
|
return 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'; 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,16);})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickSave(c) {
|
||||||
|
await c.evalJS(`window.scrollTo(0,0)`); await wait(500);
|
||||||
|
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 !!b;})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 物流属性:先保存一次逼出「请选择物流属性」,点它的编辑入口 → 勾「普货」→ 确定 */
|
||||||
|
async function fixLogistics(c) {
|
||||||
|
const pos = JSON.parse(String(await c.evalJS(`(()=>{
|
||||||
|
const bad=[...document.querySelectorAll('.ant-form-item-has-error')].find(e=>/物流属性/.test(e.innerText||''));
|
||||||
|
if(!bad) return '[]';
|
||||||
|
const link=bad.querySelector('.icon_edit2')||bad.querySelector('i.link');
|
||||||
|
if(!link) return '[]'; link.scrollIntoView({block:'center'});
|
||||||
|
const r=link.getBoundingClientRect(); return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`)));
|
||||||
|
if (!pos.length) return 'no-entry';
|
||||||
|
await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: pos[0], y: pos[1], buttons: 0 }); await wait(300);
|
||||||
|
for (const t of ['mousePressed', 'mouseReleased'])
|
||||||
|
await c.send('Input.dispatchMouseEvent', { type: t, x: pos[0], y: pos[1], button: 'left', clickCount: 1, buttons: 1 });
|
||||||
|
await wait(3500);
|
||||||
|
const tick = await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
|
||||||
|
if(!m) return 'no-modal';
|
||||||
|
const ws=[...m.querySelectorAll('.ant-checkbox-wrapper')];
|
||||||
|
const w=ws.find(x=>/普货/.test(x.innerText||''))||ws[0];
|
||||||
|
if(!w) return 'no-checkbox';
|
||||||
|
for(const ev of ['mousedown','mouseup','click']) w.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
|
||||||
|
const i=w.querySelector('input'); if(i&&!i.checked) i.click();
|
||||||
|
const ok=[...m.querySelectorAll('button')].find(x=>/^\\s*确定\\s*$/.test((x.innerText||'').trim()));
|
||||||
|
if(ok) for(const ev of ['mousedown','mouseup','click']) ok.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
|
||||||
|
return 'ticked:'+(w.innerText||'').trim().slice(0,10);})()`);
|
||||||
|
await wait(3000);
|
||||||
|
return tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`aliChoice 批量开始:任务 ${tasks.length},dry=${dry}`);
|
||||||
|
for (const t of tasks) {
|
||||||
|
if (!dry && done[t.pid] && done[t.pid].ok) { log(`跳过 ${t.pid}(已建)`); continue; }
|
||||||
|
const it = INPUT[t.pid]; if (!it) { log(`✗ ${t.pid} 无输入`); continue; }
|
||||||
|
log(`── ${t.pid} kw=${it.cat_kw || '?'} :: ${String(it.title).slice(0, 40)}`);
|
||||||
|
let c;
|
||||||
|
try {
|
||||||
|
c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add');
|
||||||
|
await wait(12000);
|
||||||
|
await installHook(c);
|
||||||
|
log(` 店铺: ${await pickFirstOption(c, '店铺名称')}`);
|
||||||
|
await wait(6000);
|
||||||
|
log(` 备货类型: ${await pickFirstOption(c, '备货类型')}`);
|
||||||
|
await wait(3000);
|
||||||
|
const cat = await selectCategory(c, it.cat_kw || '发夹', it.cat_kw);
|
||||||
|
log(` 类目: ${String(cat).slice(0, 110)}`);
|
||||||
|
if (!String(cat).startsWith('path:')) throw new Error('选类目失败:' + cat);
|
||||||
|
await wait(6000);
|
||||||
|
const fill = await c.evalJS(`(()=>{const p=${P}; const imgs=${JSON.stringify((it.imgs || []).slice(0, 6))};
|
||||||
|
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)=>{ r.skuPrice=String(${JSON.stringify(String(it.price_cny || '19.90'))});
|
||||||
|
r.skuCode='AC-'+${JSON.stringify(t.pid)}+'-'+(i+1); r.packageWeight='100';
|
||||||
|
r.packageLength='20'; r.packageWidth='15'; r.packageHeight='3'; });
|
||||||
|
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
|
||||||
|
if(desc&&!desc.detail) desc.detail=${JSON.stringify((it.bullets || []).join('<br/>'))};
|
||||||
|
return JSON.stringify({subject:b.subject.slice(0,16), imgs:pr.mainImage.length, rows:(sd.skuDataList||[]).length});})()`);
|
||||||
|
log(` 填充: ${fill}`);
|
||||||
|
await wait(3500);
|
||||||
|
const attrs = await fillRequiredSelects(c);
|
||||||
|
if (attrs !== 'none') log(` 必填属性: ${String(attrs).slice(0, 150)}`);
|
||||||
|
await wait(1200);
|
||||||
|
log(` PC端描述: ${await fillDescription(c, (it.bullets || []).join('<br/>') || it.title)}`);
|
||||||
|
await wait(1500);
|
||||||
|
if (dry) { delete done[t.pid]; log(' ✓ dry 构建完成(未保存)'); await closePage(c); continue; }
|
||||||
|
// 保存 → 若报「请选择物流属性」就补上 → 再保存
|
||||||
|
await clickSave(c);
|
||||||
|
await wait(11000);
|
||||||
|
let mods = String(await c.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,110)))`));
|
||||||
|
if (!/已保存|保存成功|待发布/.test(mods)) {
|
||||||
|
const lg = await fixLogistics(c);
|
||||||
|
log(` 物流属性: ${lg}`);
|
||||||
|
await clickSave(c);
|
||||||
|
await wait(12000);
|
||||||
|
mods = String(await c.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,110)))`));
|
||||||
|
}
|
||||||
|
const cap = await readHook(c);
|
||||||
|
const ok = /已保存|保存成功|待发布/.test(mods);
|
||||||
|
done[t.pid] = ok ? { ok: true } : { ok: false, mods: mods.slice(0, 160), hook: String(cap).slice(-240) };
|
||||||
|
log(ok ? ' ✓ 已保存到待发布' : ` ✗ 失败 ${mods.slice(0, 110)} | ${String(cap).slice(-170)}`);
|
||||||
|
} catch (e) {
|
||||||
|
done[t.pid] = { ok: false, msg: String(e).slice(0, 240) };
|
||||||
|
log(` ✗ 异常: ${String(e).slice(0, 220)}`);
|
||||||
|
} finally {
|
||||||
|
if (!dry) writeFileSync(DONE_F, JSON.stringify(done, null, 1));
|
||||||
|
if (c) await closePage(c);
|
||||||
|
await wait(2500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(`aliChoice 批量结束:成功 ${Object.values(done).filter(x => x.ok).length}/${tasks.length}`);
|
||||||
Reference in new issue
Block a user