凡科/华可商城「产品中心」广告位配图(装修编辑器)
把「打开装修编辑器 → 选中产品中心模块 → 点击广告位 → 图片设置 → 素材库选图 → 确认 → 保存草稿」整条链路脚本化、无头自动化。
适用后台特征
- 商城后台:
*.mallapp.vip.webportal.top装修编辑器 SPA(hash 路由) - 本次目标页:
https://aluo01-6.mallapp.vip.webportal.top/#/category - 技术栈:Vue 2.6.14 + Ant Design Vue + 全局 Vuex
window.$store - 素材弹窗是独立 Vue 实例
window.materialApp
三大坑(趟平了,照做即可)
-
素材弹窗里的图片点选不触发选中
直接用el.click()、MouseEvent、force: true、真实坐标点击都无效,
Vue 的@click处理器只在受信点击或内部chooseFile上更新chosenMaterialList。
→ 必须用window.materialApp.chooseFile(fileObj)直接写状态。 -
Playwright 的
filechooser/setInputFiles在此上传入口无效
无论是面板 upload 触发器还是素材弹窗的「上传文件」按钮,都不会触发原生文件选择器。
→ 配图优先用素材库已有图片;如需传新图,走 FaiUpload 分块上传协议(见webportal-mall-product-editskill)把图先进素材库,再回来选。 -
素材弹窗的「确定」按钮 class 是
fa-popup--footer .fa-btn-primary,文本带空格「确 定」
用文本匹配/确定/会漏掉;建议直接用 class 选择器。
一、核心 DOM 与状态
入口与广告位
const DECORATOR_URL = 'https://aluo01-6.mallapp.vip.webportal.top/#/category';
const PRODUCT_LIST_MODULE = '.fa-designer-dnd-drag-handler.productList';
const AD_PLACEHOLDER = '.fk_product_category_advImg'; // 点击后打开「图片设置」
const AD_IMG_HOLDER = '.fk_product_category_advImg_img_404'; // 无图时的占位 div
const UPLOAD_TRIGGER = '.product_adv_upload .fa-upload span.fa-upload';
素材弹窗
const POPUP_ROOT = '.ma_material'; // 最外层
const POPUP_OK = '.fa-popup--footer .fa-btn-primary'; // 确 定
const POPUP_CANCEL = '.fa-popup--footer .fa-popup--button:nth-child(2)';
const FILE_ITEM = '.fa-basic-upload-file-view-list-item';
Vue 全局对象
window.Vue.version; // "2.6.14"
window.$store; // Vuex store,含 rightPanel / productCategoryModule 等
window.materialApp; // 素材弹窗 Vue 根实例
materialApp._data.fmInfoList; // 当前「我的文件」列表
materialApp.chosenMaterialList; // 已选图片(最多 maxChosenFileCount 张)
materialApp.maxChosenFileCount; // 广告位单图为 1
materialApp.chooseFile(fileObj); // 选中某张图
materialApp.onFileChoose(fileObj); // 备选方法
fmInfoList 文件对象结构
{
"__TYPE": "jpg",
"materialId": "ABUIABACGAAgnIf61AYoqPvw4gIwoAY4oAY",
"materialPath": "//34133765.s21i.faimallusr.com/2/6/ABUIABACGAAgnIf61AYoqPvw4gIwoAY4oAY.jpg",
"materialPreviewPath": "//34133765.s21i.faimallusr.com/2/6/ABUIABACGAAgnIf61AYoqPvw4gIwoAY4oAY!100x100.jpg",
"materialName": "beer_main.jpg",
"materialWidth": 800,
"materialHeight": 800,
"type0": "jpg",
"oldFormatFile": { "fileId": "...", "filePath": "...", "fileName": "..." }
}
二、端到端脚本
const { chromium } = require('playwright-core');
const fs = require('fs');
const path = require('path');
const CHROME = 'C:\\Users\\Administrator\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe';
const OUT = process.env.WM_OUT || __dirname;
const STATE = path.join(OUT, 'state.json');
const sleep = ms => new Promise(r => setTimeout(r, ms));
// 目标:把素材库里的 beer_main 设为产品中心广告位图片
const TARGET_MATERIAL_ID = 'ABUIABACGAAgnIf61AYoqPvw4gIwoAY4oAY';
(async () => {
for (const k of ['HTTP_PROXY','HTTPS_PROXY','http_proxy','https_proxy','ALL_PROXY','all_proxy']) delete process.env[k];
const ctx = await chromium.launchPersistentContext(path.join(OUT, '_profile'), {
executablePath: CHROME, headless: true,
viewport: { width: 1440, height: 900 },
args: ['--no-sandbox','--disable-dev-shm-usage','--ignore-certificate-errors','--disable-blink-features=AutomationControlled'],
ignoreHTTPSErrors: true,
});
const page = ctx.pages()[0] || await ctx.newPage();
page.setDefaultTimeout(45000);
const state = JSON.parse(fs.readFileSync(STATE, 'utf8'));
if (state.cookies) await ctx.addCookies(state.cookies);
await page.goto('https://aluo01-6.mallapp.vip.webportal.top/#/category', { waitUntil: 'networkidle' });
await sleep(4000);
// 关闭「新版本正在微信审核中」提示
await page.evaluate(() => {
const b = [...document.querySelectorAll('button,span')].find(b => /我知道了/.test(b.innerText));
if (b) b.click();
});
await sleep(500);
// 1. 选中产品列表模块并点开广告位
await page.evaluate(() => document.querySelector('.fa-designer-dnd-drag-handler.productList')?.click());
await sleep(800);
await page.evaluate(() => document.querySelector('.fk_product_category_advImg')?.click());
await sleep(2000);
// 2. 打开素材弹窗
await page.evaluate(() => document.querySelector('.product_adv_upload .fa-upload span.fa-upload')?.click());
await sleep(4000);
await page.waitForSelector('.fa-basic-upload-file-view-list-item', { timeout: 15000 });
await sleep(1500);
// 3. 用 Vue 内部方法选中目标图片(唯一可靠方式)
await page.evaluate((id) => {
const ma = window.materialApp;
const list = ma._data.fmInfoList || [];
const idx = list.findIndex(f => f.materialId === id);
if (idx < 0) throw new Error('目标素材不在当前列表:' + id);
ma.chooseFile(list[idx]);
}, TARGET_MATERIAL_ID);
await sleep(1500);
// 4. 点击素材弹窗「确 定」把图片回填到「图片设置」面板
await page.locator('.fa-popup--footer .fa-btn-primary').first().click({ timeout: 5000 });
await sleep(2000);
// 5. 关闭素材弹窗(如未自动关闭,点取消)
try {
await page.locator('.fa-popup--footer .fa-popup--button').nth(1).click({ timeout: 3000 });
} catch (e) {
await page.evaluate(() => {
const btns = document.querySelectorAll('.fa-popup--footer .fa-popup--button');
if (btns.length > 1) btns[1].click();
});
}
await sleep(1500);
// 6. 点图片设置面板确认
await page.evaluate(() => {
const panel = document.querySelector('.fa-designer-panel');
if (!panel) return;
const btn = [...panel.querySelectorAll('button,span,div')]
.find(b => /确定|保存/.test(b.innerText.replace(/\\s/g, '')) && !/取消/.test(b.innerText));
if (btn) btn.click();
});
await sleep(2000);
// 7. 保存装修草稿(顶部「保存」按钮)
await page.evaluate(() => {
const btn = [...document.querySelectorAll('button,span,div')]
.find(b => /保存/.test(b.innerText) && !/取消/.test(b.innerText) && b.offsetWidth > 0);
if (btn) btn.click();
});
await sleep(3000);
// 8. 验证:重开广告位,看面板是否仍带图
await page.evaluate(() => document.querySelector('.fa-designer-dnd-drag-handler.productList')?.click());
await sleep(800);
await page.evaluate(() => document.querySelector('.fk_product_category_advImg')?.click());
await sleep(1000);
const verify = await page.evaluate(() => ({
panelText: document.querySelector('.fa-designer-panel')?.innerText?.slice(0, 300) || '',
hasImg: !!document.querySelector('.fa-designer-panel img'),
imgSrc: document.querySelector('.fa-designer-panel img')?.src || '',
}));
console.log('VERIFY:', verify);
await ctx.close();
})();
三、如何用脚本选特定图片
按 materialId 匹配最稳;也可按 materialName:
const idx = list.findIndex(f => f.materialName === 'beer_main.jpg');
ma.chooseFile(list[idx]);
如需多图(多广告位/轮播),逐个 chooseFile,但广告位通常 maxChosenFileCount=1,多选会超出。
四、验证清单
- [ ] 打开装修编辑器
/#/category页面,产品中心模块上方出现目标图片 - [ ] 右侧「图片设置」面板出现图片预览、图片缩放/链接/大图选项
- [ ] 顶部「保存」按钮点击后无报错,草稿保存成功
- [ ] 重新打开广告位,面板仍显示同一图片
五、易错速查
| 现象 | 根因 | 修法 |
|---|---|---|
| 广告位仍是平台占位图 | 面板确认后未点顶部「保存」 | 补点保存 |
| 素材弹窗点了没反应 | 用 el.click() 没触发 Vue 选中 |
改用 materialApp.chooseFile(fileObj) |
| 找不到「确定」按钮 | 按钮文本是「确 定」带空格 | 用 .fa-popup--footer .fa-btn-primary |
| 弹窗关闭后图片设置面板空白 | 先关了弹窗再点面板确认 | 顺序:选图 → 弹窗确定 → 弹窗取消 → 面板确定 → 保存 |
| 需要上新 banner | filechooser 不弹出 | 走 FaiUpload 分块上传把图先传到素材库,再回来选 |
六、配色约束
本 skill 涉及的所有视觉/前端资产(流程图、演示 HTML、CSS 示例、截图批注)必须遵守:
> 去除所有黑色:任何位置都不得使用 #000000 / rgb(0,0,0) / 纯黑。 深色 UI 用白色或浅色系替代;文字也不得为纯黑,统一用非纯黑墨色。
具体执行:
- 背景 / 卡片 / 弹窗 / 边框:以白色 #FFFFFF 或带浅渐变、毛玻璃的浅色面替代原来的深色;可叠极淡噪点或 1px 浅灰 #E5E7EB 描边营造纵深,避免死白平涂。
- 文字墨色:#1F2937(slate-800)/ #374151(slate-700),对比度 ≥ 4.5:1,绝不是 #000。
- 品牌主色:绿 #00B578(高对比 CTA / 强调);次色浅灰绿 #E7F7F0;点缀琥珀 #F59E0B。
- 禁止项:纯黑 #000、纯灰大面积平铺(可用浅灰作描边/分隔,不作底色)。
> 注:本约束优先级高于「通用前端审美里避免纯白背景」的偏好——用户明确要求「去除黑色、先用白色替换」,以白色为 interim 底色,再用浅渐变/毛玻璃补纵深。
七、安全
state.json/_TOKEN是会话凭据,勿入库、勿外传。- 操作会直接改写线上商城装修草稿,正式变更前先在测试站点验证。
- 顶部「发布新版本」在微信审核期间可能被灰度拦截;保存草稿不影响线上。