微信转发图:百问专题/计算器/趣味小测/孕妈广场 357 张 5:4 转发图(logo+核心词大字),页面上报 imageUrl(v26110296)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
46
tools/share-card/build.js
Normal file
46
tools/share-card/build.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// 生成转发图清单:百问专题 / 计算器 / 测试 / 广场笔记 / 广场话题
|
||||
const fs = require('fs'), R = '/work/mama-box/';
|
||||
global.window = {};
|
||||
const ev = f => eval(fs.readFileSync(R + f, 'utf8'));
|
||||
ev('guangchang/data.js'); ev('guangchang/topics.js'); ev('guangchang/ceshi/meta.js');
|
||||
const hot = require(R + 'assets/data/qa/hot.json');
|
||||
const src = fs.readFileSync(R + 'js/app.js', 'utf8');
|
||||
const kwSrc = src.slice(src.indexOf('const QA_KW = {') + 14, src.indexOf('};', src.indexOf('const QA_KW = {')) + 1);
|
||||
const QA_KW = eval('(' + kwSrc.replace(/\/\*[\s\S]*?\*\//g, '') + ')');
|
||||
const KW2 = { ceshi: '哪种妈妈', wen: '妊娠纹', zhichang: '复工维权', shaicha: '唐筛无创', kafei: '咖啡奶茶', ranfa: '染发烫发', taidong: '初次胎动',
|
||||
fenmian: '顺产剖宫', daichanbao: '待产包', shimian: '孕期失眠', tongfang: '孕期同房', zhunbaba: '准爸爸', pinxue: '孕期贫血', gaoxueya: '血压高',
|
||||
baidai: '白带增多', taijiao: '胎教', yuezi: '坐月子', hcg: 'HCG孕酮', yuchanqi: '预产期', tuyy: '孕吐营养', futong: '肚子隐痛', jikou: '孕早期忌口',
|
||||
xianhuai: '显怀', bugai: '补钙补铁', weikou: '控制体重', zhangban: '长斑变黑', touyun: '头晕心慌', yunfu: '孕妇装', gongjing: '宫颈短', fansuan: '烧心反酸',
|
||||
chigu: '耻骨痛', jiagong: '假性宫缩', taidongshao: '胎动变少', yangshui: '羊水', wutong: '无痛分娩', poshui: '破水', jianhongsheng: '见红待产', ruyuan: '入院证件',
|
||||
kainai: '开奶', chusheng: '出生打针', duiyou: '队友类型', yali: '孕期压力', yinshi: '饮食打分', zhunbei: '准备几成', jisuan: '孕期计算器', 'yuchanqi-js': '预产期' };
|
||||
const TAGC = { 身体: '#EE6A8C', 准备: '#6C78E6', 产检: '#3C8BE6', 宝宝: '#FF8F3F', 生活: '#2FAE84', 营养: '#4BAE5E', 职场: '#5B6FD8', 花钱: '#F0962A', 心情: '#E66B9A', 趣味: '#9A7FE0', 补剂: '#3C9BD8', 专题: '#22A3A0' };
|
||||
const QUIZ_TOPIC = { ceshi: 1, duiyou: 1, yali: 1, yinshi: 1, zhunbei: 1 };
|
||||
const CALC = { huafei: 1, jisuan: 1, 'yuchanqi-js': 1 };
|
||||
const out = [];
|
||||
const tt = d => (fs.readFileSync(R + 'topic/' + d + '/index.html', 'utf8').match(/<title>([^<]*)/) || [])[1] || '';
|
||||
const dirs = fs.readdirSync(R + 'topic').filter(d => fs.existsSync(R + 'topic/' + d + '/index.html'));
|
||||
dirs.forEach(d => {
|
||||
const h = hot.find(x => x.href === 'topic/' + d + '/');
|
||||
const q = h ? h.q : tt(d).replace(/^测一测:/, '').replace(/\s*[·||].*$/, '');
|
||||
const kw = (h && QA_KW[h.q]) || KW2[d];
|
||||
if (!kw) throw new Error('no kw ' + d);
|
||||
let mod = '孕期百问', c = TAGC[h ? h.tag : '趣味'] || '#22A3A0', cta = '点开看答案';
|
||||
if (QUIZ_TOPIC[d]) { mod = '测一测'; c = '#9A7FE0'; cta = '点开测一测'; }
|
||||
if (CALC[d]) { mod = '孕期计算器'; c = '#F57C4C'; cta = '马上算一算'; }
|
||||
if (d === 'jisuan') { out.push({ k: 't/' + d, type: 'text', mod, c, big: '孕期计算器', title: '生娃花费、预产期、产假天数,一算就知道', cta, ic: 'calc' }); return; }
|
||||
if (d === 'yuchanqi-js') { out.push({ k: 't/' + d, type: 'text', mod, c, big: '预产期', title: '宝宝哪天到?还有几天见面', cta, ic: 'calc' }); return; }
|
||||
out.push({ k: 't/' + d, type: 'text', mod, c, big: kw, title: q, cta, ic: CALC[d] ? 'calc' : QUIZ_TOPIC[d] ? 'quiz' : (h ? h.tag : '专题') });
|
||||
});
|
||||
const GC = { quiz: '#FF8A65', psy: '#9A7FE0', game: '#3FAE7A', shop: '#4E8DE6' }, GN = { quiz: '常识小测', psy: '趣味心理', game: '趣味闯关', shop: '好物配置' };
|
||||
window.GC_TESTS_META.forEach(x => out.push({ k: 'c/' + x.id, type: 'text', mod: '测一测 · ' + GN[x.group], c: GC[x.group], big: window.GC_SHORT[x.id] || '', title: x.title.replace(/^测一测|/, ''), cta: '点开测一测', ic: 'test:' + x.id, sub: x.n + ' 道题 · 测完看结果' }));
|
||||
const T = window.GC_TOPICS, D = window.GC_DATA;
|
||||
T.forEach(t => {
|
||||
out.push({ k: 'g/t-' + t.id, type: 'photo', mod: '孕妈广场 · 话题', c: t.ink || '#F4593C', bg: t.c, big: '#' + t.name + '#', title: (D[t.id] && D[t.id].desc) || '', cta: '进来聊聊', ph: t.ph ? R + t.ph : '' });
|
||||
((D[t.id] && D[t.id].notes) || []).forEach((n, i) => out.push({ k: 'g/' + t.id + '-' + (i + 1), type: 'photo', mod: '孕妈广场', c: t.ink || '#F4593C', bg: t.c, big: n.cover || n.title, sub: n.coverSub || '', title: n.title, cta: '看看她怎么说', ph: n.photo ? R + 'guangchang/' + n.photo : '' }));
|
||||
});
|
||||
out.push({ k: 'g/home', type: 'text', mod: '孕妈广场', c: '#F4593C', big: '孕妈广场', title: '孕期的那些事,听听过来人怎么说', cta: '进来看看', ic: 'plaza' });
|
||||
out.push({ k: 'g/ceshi', type: 'text', mod: '测一测', c: '#8D6BFF', big: '趣味小测试', title: '30 个孕育小测试,1 分钟测出你是哪种孕妈', cta: '点开测一测', ic: 'quiz' });
|
||||
const strip = v => typeof v === 'string' ? v.replace(/[\p{Extended_Pictographic}\uFE0F\u200D]/gu, '').replace(/\s{2,}/g, ' ').trim() : v;
|
||||
out.forEach(x => ['big', 'title', 'sub'].forEach(k => { x[k] = strip(x[k]); }));
|
||||
fs.writeFileSync('/work/mama-box/tools/share-card/manifest.json', JSON.stringify(out, null, 1));
|
||||
console.log(out.length, out.filter(x => x.type === 'photo' && !x.ph).length);
|
||||
1
tools/share-card/icons.json
Normal file
1
tools/share-card/icons.json
Normal file
File diff suppressed because one or more lines are too long
29
tools/share-card/render.py
Normal file
29
tools/share-card/render.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import asyncio,os,json,sys
|
||||
from playwright.async_api import async_playwright
|
||||
M=json.load(open('/work/mama-box/tools/share-card/manifest.json'));I=json.load(open('/work/mama-box/tools/share-card/icons.json'))
|
||||
OUT='/work/mama-box/assets/share/'
|
||||
GEN={'calc':'<rect x="5" y="2.5" width="14" height="19" rx="3"/><rect x="7.5" y="5" width="9" height="4" rx="1" fill="rgba(0,0,0,.22)"/><g fill="rgba(0,0,0,.22)"><circle cx="9" cy="12.5" r="1.2"/><circle cx="12" cy="12.5" r="1.2"/><circle cx="15" cy="12.5" r="1.2"/><circle cx="9" cy="16" r="1.2"/><circle cx="12" cy="16" r="1.2"/><circle cx="15" cy="16" r="1.2"/></g>',
|
||||
'quiz':'<path d="M12 2.8l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 16.8l-5.6 2.9 1-6.1-4.4-4.3 6.1-.9z"/>',
|
||||
'plaza':'<path d="M4 4h16a2 2 0 012 2v10a2 2 0 01-2 2h-8l-5 4v-4H4a2 2 0 01-2-2V6a2 2 0 012-2z"/><path d="M7 9h10M7 13h6" stroke="rgba(0,0,0,.22)" stroke-width="1.8" stroke-linecap="round"/>'}
|
||||
def svg(x):
|
||||
ic=x.get('ic') or ''
|
||||
if ic.startswith('test:'): p=I['TI'][ic[5:]]
|
||||
elif ic in GEN: p=GEN[ic]
|
||||
else: p=I['IC'].get(ic) or GEN['quiz']
|
||||
return '<svg viewBox="0 0 24 24" fill="#fff">'+p+'</svg>'
|
||||
async def main():
|
||||
only=sys.argv[1:]
|
||||
async with async_playwright() as p:
|
||||
b=await p.chromium.launch(executable_path=os.environ.get('CH'),args=['--allow-file-access-from-files'])
|
||||
pg=await b.new_page(viewport={'width':750,'height':600})
|
||||
await pg.goto('file:///work/mama-box/tools/share-card/tpl.html')
|
||||
n=0
|
||||
for x in M:
|
||||
if only and x['k'] not in only and not any(x['k'].startswith(o) for o in only): continue
|
||||
await pg.evaluate("([x,s])=>window.draw(x,s)",[x,svg(x)])
|
||||
await pg.evaluate("document.fonts.ready")
|
||||
fn=OUT+x['k']+'.jpg'; os.makedirs(os.path.dirname(fn),exist_ok=True)
|
||||
await pg.screenshot(path=fn,type='jpeg',quality=84,clip={'x':0,'y':0,'width':750,'height':600}); n+=1
|
||||
print('rendered',n)
|
||||
await b.close()
|
||||
asyncio.run(main())
|
||||
55
tools/share-card/tpl.html
Normal file
55
tools/share-card/tpl.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{width:750px;height:600px;font-family:"Noto Sans CJK SC","Noto Sans CJK",sans-serif;background:#fff}
|
||||
#c{position:relative;width:750px;height:600px;overflow:hidden;--c:#22A3A0}
|
||||
.t{background:linear-gradient(160deg,color-mix(in srgb,var(--c) 16%,#fff) 0%,#fff 52%,color-mix(in srgb,var(--c) 10%,#fff) 100%)}
|
||||
.t::before{content:"";position:absolute;inset:0;background-image:radial-gradient(color-mix(in srgb,var(--c) 22%,#fff) 2px,transparent 2.4px);background-size:26px 26px;opacity:.6}
|
||||
.blob{position:absolute;border-radius:50%;background:color-mix(in srgb,var(--c) 18%,#fff)}
|
||||
.b1{width:360px;height:360px;right:-120px;top:-150px}.b2{width:220px;height:220px;left:-90px;bottom:-110px;background:color-mix(in srgb,var(--c) 12%,#fff)}
|
||||
.logo{position:absolute;left:40px;top:34px;height:62px;padding:8px 18px;border-radius:20px;background:#fff;box-shadow:0 6px 18px -8px rgba(0,0,0,.18);display:flex;align-items:center}
|
||||
.logo img{height:46px}
|
||||
.chip{position:absolute;right:40px;top:44px;height:46px;padding:0 22px;border-radius:99px;background:var(--c);color:#fff;font-size:24px;font-weight:700;line-height:46px;letter-spacing:1px}
|
||||
.big{position:absolute;left:46px;top:128px;width:480px;height:224px;display:flex;align-items:center}
|
||||
.bt{width:100%;text-wrap:balance}
|
||||
.big span{display:inline;font-weight:900;color:color-mix(in srgb,var(--c) 78%,#1b1b1b);letter-spacing:4px;line-height:1.12;background:linear-gradient(transparent 68%,color-mix(in srgb,var(--c) 26%,#fff) 68%,color-mix(in srgb,var(--c) 26%,#fff) 94%,transparent 94%);-webkit-box-decoration-break:clone}
|
||||
.tile{position:absolute;right:52px;top:176px;width:150px;height:150px;border-radius:42px;background:var(--c);transform:rotate(-8deg);display:grid;place-items:center;box-shadow:0 18px 30px -14px var(--c)}
|
||||
.tile svg{width:92px;height:92px}
|
||||
.ttl{position:absolute;left:48px;top:372px;width:654px;font-size:34px;font-weight:700;color:#2B2320;line-height:1.38;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.sub{display:none}
|
||||
.cta{position:absolute;left:46px;bottom:40px;height:64px;padding:0 34px;border-radius:99px;background:var(--c);color:#fff;font-size:30px;font-weight:900;line-height:64px;letter-spacing:2px;box-shadow:0 12px 22px -12px var(--c)}
|
||||
.slg{position:absolute;right:44px;bottom:58px;font-size:22px;color:#A89C96;letter-spacing:1px}
|
||||
/* photo */
|
||||
.p{background:linear-gradient(160deg,var(--l1),#fff 60%,var(--l2))}
|
||||
.p .ph{position:absolute;right:0;top:0;width:330px;height:600px;object-fit:cover}
|
||||
.p .pl{position:absolute;left:46px;top:122px;bottom:36px;width:350px;display:flex;flex-direction:column;align-items:flex-start;gap:14px}
|
||||
.p .chip{position:static;height:40px;line-height:40px;font-size:21px;padding:0 18px}
|
||||
.p .big{position:static;width:100%;height:auto;flex:1;min-height:0;align-items:center}
|
||||
.p .big span{letter-spacing:1px;line-height:1.18}
|
||||
.p .sub2{font-size:23px;color:#8A7F7A;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-4px}
|
||||
.p .ttl{position:static;width:100%;font-size:23px;font-weight:500;color:#5B504B;-webkit-line-clamp:2;line-height:1.4}
|
||||
.p .cta{position:static;height:58px;line-height:58px;font-size:27px;padding:0 28px;flex:none}
|
||||
</style></head><body><div id="c"></div>
|
||||
<script>
|
||||
var LOGO='file:///work/mama-box/assets/img/brand-logo-c.png';
|
||||
function esc(s){return String(s||'').replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||||
function fit(el,max,min,lines){var s=el.querySelector('span');for(var f=max;f>=min;f-=2){s.style.fontSize=f+'px';var n=0,r=s.getClientRects(),ys={};for(var i=0;i<r.length;i++)ys[Math.round(r[i].top)]=1;n=Object.keys(ys).length;
|
||||
if(n<=lines&&el.scrollHeight<=el.clientHeight+2&&s.parentNode.scrollWidth<=s.parentNode.clientWidth+1)return f}return min}
|
||||
window.draw=function(x,svg){
|
||||
var c=document.getElementById('c');c.style.setProperty('--c',x.c);
|
||||
if(x.type==='photo'){
|
||||
var l1=(x.bg&&x.bg[0])||'#FFE6EB',l2=(x.bg&&x.bg[1])||'#FFC6D2';
|
||||
c.className='p';c.style.setProperty('--l1',l1);c.style.setProperty('--l2',l2);c.style.setProperty('--l1x',l1);
|
||||
c.innerHTML='<img class="ph" src="file://'+x.ph+'"><div class="phm" style="background:linear-gradient(90deg,'+l1+' 0%,transparent 100%);right:270px;width:60px;display:none"></div>'+
|
||||
'<div class="logo"><img src="'+LOGO+'"></div><div class="pl"><div class="chip">'+esc(x.mod)+'</div><div class="big"><div class="bt"><span>'+esc(x.big)+'</span></div></div>'+
|
||||
(x.sub?'<div class="sub2">'+esc(x.sub)+'</div>':'')+'<div class="ttl">'+esc(x.title)+'</div><div class="cta">'+esc(x.cta)+' ›</div></div>';
|
||||
fit(c.querySelector('.big'),84,36,3);
|
||||
}else{
|
||||
c.className='t';
|
||||
c.innerHTML='<i class="blob b1"></i><i class="blob b2"></i><div class="logo"><img src="'+LOGO+'"></div><div class="chip">'+esc(x.mod)+'</div>'+
|
||||
'<div class="tile">'+svg+'</div><div class="big"><div class="bt"><span>'+esc(x.big)+'</span></div></div><div class="ttl">'+esc(x.title)+'</div>'+
|
||||
(x.sub?'<div class="sub">'+esc(x.sub)+'</div>':'')+'<div class="cta">'+esc(x.cta)+' ›</div><div class="slg">陪你从孕期到育儿的每一天</div>';
|
||||
fit(c.querySelector('.big'),150,56,1)>=96||fit(c.querySelector('.big'),110,50,2);
|
||||
}
|
||||
return Promise.all([].map.call(c.querySelectorAll('img'),function(i){return i.complete?1:new Promise(function(r){i.onload=i.onerror=r})}));
|
||||
};
|
||||
</script></body></html>
|
||||
Reference in New Issue
Block a user