Files
juzi/tools/build_taobao_proposal_from_template.py
T
xsl 2eb0d8e807 docs(proposal): add Taobao product proposal decks and their generators
产品方案 PPT 是交付物,之前只存在于本机,换开发机后会丢,所以入库。

- 3 份 pptx:智能测肤方案(正版 1.3MB / 深色版 1.3MB)+ 购物小程序方案模版(0.1MB);
  模版是 build_taobao_proposal_from_template.py 的输入,必须一起入库
- 3 个生成脚本:把硬编码的 D:\juzi\... 改为相对仓库根(Path(__file__).parents[1]),
  渲染用的截图素材 temp/ppt_assets 属临时产物,加进 .gitignore
- build_taobao_proposal_pptx.py 是更早的一版(产物 AI智能测肤...pptx 当前不在仓库),
  保留用于追溯版式
- 新增 .gitignore:/sdk-java-35479221/(淘宝开放平台自动生成的 Java SDK,
  对应已废弃的 AppKey 35479221,当前架构走 HTTP 透传,不引 SDK)
2026-11-01 23:37:38 +08:00

401 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Clone 淘宝购物小程序方案模板 -> AI智能测肤淘宝小程序产品方案(不出现公司名)。
模板设计语言(继承):
- 画布 26.67x15in;封面/致谢 FF5500->3768E2 渐变(原样保留)
- 章节页:白底 + DIN Condensed 120pt 大数字 + FF6F00 橙色标题(Heiti SC Light)
- 目录页:FF644E 珊瑚红圆圈编号(accent5),原样保留
新增元素仅使用:ORANGE 标题 / CORAL 强调 / 黑灰正文 / 浅灰卡片,保证视觉一致。
截图统一裁掉顶部 220px 导航栏(去除品牌名与微信胶囊按钮)。
"""
import copy
from pathlib import Path
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.oxml.ns import qn
# 路径相对仓库根,换机器无需改(截图素材在 temp/ppt_assets,属于本地临时产物,不入库)
_ROOT = Path(__file__).resolve().parents[1]
TPL = str(_ROOT / "购物小程序方案-模版.pptx")
OUT = str(_ROOT / "智能测肤-淘宝小程序产品方案.pptx")
AST = str(_ROOT / "temp" / "ppt_assets")
ORANGE = RGBColor(0xFF, 0x6F, 0x00)
CORAL = RGBColor(0xFF, 0x64, 0x4E)
INK = RGBColor(0x1A, 0x1A, 0x1A)
GREY = RGBColor(0x5E, 0x5E, 0x5E)
LGREY = RGBColor(0xD5, 0xD5, 0xD5)
CARD = RGBColor(0xF7, 0xF7, 0xF7)
BAND = RGBColor(0xF2, 0xF2, 0xF2)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
NUM_F = "DIN Condensed" # 与模板大数字一致(缺失时由查看端统一替换)
# ---------- 基础设施 ----------
def set_font(run, size, bold=False, color=INK, latin="Helvetica Neue", ea="微软雅黑"):
f = run.font
f.size = Pt(size)
f.bold = bold
f.color.rgb = color
f.name = latin
rPr = run._r.get_or_add_rPr()
for tag, face in ((qn("a:latin"), latin), (qn("a:ea"), ea)):
e = rPr.find(tag)
if e is None:
e = rPr.makeelement(tag, {})
rPr.append(e)
e.set("typeface", face)
def tb(slide, x, y, w, h, paras, anchor=MSO_ANCHOR.TOP):
"""paras: list of dict(t,size,bold,color,latin,ea,sa,align)"""
box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
tf = box.text_frame
tf.word_wrap = True
tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = 0
tf.vertical_anchor = anchor
for i, pd in enumerate(paras):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.alignment = pd.get("align", PP_ALIGN.LEFT)
if pd.get("sa"):
p.space_after = Pt(pd["sa"])
r = p.add_run()
r.text = pd["t"]
set_font(r, pd.get("size", 26), pd.get("bold", False), pd.get("color", INK),
pd.get("latin", "Helvetica Neue"), pd.get("ea", "微软雅黑"))
return box
def card(slide, x, y, w, h, fill=CARD, line=LGREY):
sp = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h))
sp.adjustments[0] = 0.055
sp.fill.solid()
sp.fill.fore_color.rgb = fill
if line:
sp.line.color.rgb = line
sp.line.width = Pt(1)
else:
sp.line.fill.background()
sp.shadow.inherit = False
return sp
def band(slide, x, y, w, h, fill=BAND):
sp = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h))
sp.fill.solid()
sp.fill.fore_color.rgb = fill
sp.line.fill.background()
sp.shadow.inherit = False
return sp
def circle(slide, x, y, d, label, fill=CORAL, color=WHITE, size=26, latin=NUM_F):
sp = slide.shapes.add_shape(MSO_SHAPE.OVAL, Inches(x), Inches(y), Inches(d), Inches(d))
sp.fill.solid()
sp.fill.fore_color.rgb = fill
sp.line.fill.background()
sp.shadow.inherit = False
tf = sp.text_frame
tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = 0
tf.vertical_anchor = MSO_ANCHOR.MIDDLE
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
r = p.add_run()
r.text = label
set_font(r, size, True, color, latin, "微软雅黑")
return sp
def pic(slide, path, x, y, w, h):
p = slide.shapes.add_picture(path, Inches(x), Inches(y), Inches(w), Inches(h))
p.line.color.rgb = LGREY
p.line.width = Pt(1)
return p
def find(slide, needle):
for sh in slide.shapes:
if sh.has_text_frame and needle in sh.text_frame.text:
return sh
raise KeyError(needle)
def drop(shape):
shape._element.getparent().remove(shape._element)
def replace_text(shape, new, widen=None):
tf = shape.text_frame
for p in tf.paragraphs[1:]:
p._p.getparent().remove(p._p)
p = tf.paragraphs[0]
runs = p.runs
runs[0].text = new
for r in runs[1:]:
r._r.getparent().remove(r._r)
if widen:
shape.width = Inches(widen)
def clone_slide(prs, src):
new = prs.slides.add_slide(src.slide_layout)
spTree = new.shapes._spTree
for el in list(spTree):
if el.tag.split("}")[-1] in ("sp", "pic", "graphicFrame", "grpSp", "cxnSp"):
spTree.remove(el)
for shp in src.shapes:
spTree.append(copy.deepcopy(shp._element))
return new
def move_slide(prs, from_idx, to_idx):
lst = prs.slides._sldIdLst
el = list(lst)[from_idx]
lst.remove(el)
lst.insert(to_idx, el)
# ---------- 构建 ----------
def build():
prs = Presentation(TPL)
s_cover, s_pos, s_toc, s_mkt, s_val, s_biz, s_thx = prs.slides
# 第 02 章需要 3 页:克隆「小程序方案及价值」两份
feat = clone_slide(prs, s_val) # 功能与链路(配截图)
diff = clone_slide(prs, s_val) # 差异与互补
# 顺序 -> [封面,定位,目录,01,02价值,02功能,02差异,03,致谢]
move_slide(prs, 7, 5)
move_slide(prs, 8, 6)
# 大数字框在字体替换后过窄导致竖排换行+顶部裁切:删除模板框,原位重画宽框数字
for sl, num in ((s_mkt, "01"), (s_val, "02"), (feat, "02"), (diff, "02"), (s_biz, "03")):
for sh in list(sl.shapes):
if sh.has_text_frame and sh.text_frame.text.strip() == num:
drop(sh)
break
tb(sl, 0.51, 0.24, 3.2, 1.9, [dict(t=num, size=120, color=INK, latin=NUM_F)])
# 字体替换后数字变宽,章节标题统一右移让出间距
for sl, keys in ((s_mkt, ["市场调研分析"]), (s_val, ["小程序方案及价值"]),
(feat, ["小程序方案及价值"]), (diff, ["小程序方案及价值"]),
(s_biz, ["商业化运营规划及目标"])):
for k in keys:
find(sl, k).left = Inches(2.75)
# ---- P1 封面 ----
replace_text(find(s_cover, "XXXX小程序方案"), "AI智能测肤小程序方案")
comp = find(s_cover, "XXX公司")
replace_text(comp, "西安巨子生物基因技术股份有限公司", widen=17)
comp.text_frame.paragraphs[0].runs[0].font.size = Pt(64)
comp.text_frame.word_wrap = False
tb(s_cover, 7.6, 10.8, 14, 0.55,
[dict(t="淘宝开放平台小程序产品方案 · 自研类型申请", size=26, color=WHITE)])
# ---- P2 公司背景与产品定位 ----
ttl = find(s_pos, "公司背景介绍")
replace_text(ttl, "公司背景与产品定位")
ttl.width = Inches(8.0)
tb(s_pos, 10.31, 3.2, 15.5, 0.5,
[dict(t="西安巨子生物基因技术股份有限公司 · 港交所上市 · 重组胶原蛋白", size=22, color=GREY)])
drop(find(s_pos, "请一页提炼"))
# 左:完整报告长图(已裁导航栏)
pic(s_pos, f"{AST}\\report-full.png", 2.7, 2.45, 3.34, 10.29)
tb(s_pos, 1.4, 12.95, 6.0, 0.5,
[dict(t="AI 肌肤检测报告(界面示意)", size=24, color=GREY, align=PP_ALIGN.CENTER)])
# 右:一句话定位 + 三条价值
tb(s_pos, 8.1, 4.7, 16.4, 2.1,
[dict(t="让每位消费者三分钟获得", size=40, bold=True, sa=6),
dict(t="一份可信赖的肌肤报告与护理方案", size=40, bold=True)],
anchor=MSO_ANCHOR.TOP)
rows = [
("1", "测得准", "AI 图像分析输出综合评分、肌龄、七项分项与三维指标,可解释、可回看"),
("2", "推得对", "按展示肌龄分档推荐适配精华,报告页直达商品,缩短决策路径"),
("3", "留得住", "历史报告沉淀与复访提醒,支撑会员运营与复购"),
]
for i, (n, lead, desc) in enumerate(rows):
y = 7.5 + i * 2.0
circle(s_pos, 8.1, y + 0.12, 0.72, n, size=28)
tb(s_pos, 9.3, y, 15.3, 1.8,
[dict(t=lead, size=30, bold=True, sa=6),
dict(t=desc, size=24, color=GREY)])
# ---- P3 目录:模板即为目标结构,保留 ----
# ---- P4 01 市场调研分析 ----
drop(find(s_mkt, "商家价值和痛点"))
cols = [
("商家痛点", "选品讲解靠经验,「适不适合我」难讲清;直播与详情页流量难沉淀为可复访的服务。"),
("消费者诉求", "想先弄清肤质与肌龄再决策;怕踩雷、要依据,希望报告能看懂、直接告诉我买什么。"),
]
xs = [1.5, 9.72, 17.94]
for i, (t, body) in enumerate(cols):
tb(s_mkt, xs[i], 2.6, 7.2, 0.6, [dict(t=t, size=32, bold=True, color=ORANGE)])
tb(s_mkt, xs[i], 3.45, 7.2, 4.4, [dict(t=body, size=26, color=GREY)])
tb(s_mkt, xs[2], 2.6, 7.2, 0.6, [dict(t="市场与商业化空间", size=32, bold=True, color=ORANGE)])
tb(s_mkt, xs[2], 3.45, 7.2, 1.0, [dict(t="4357亿", size=54, bold=True, color=ORANGE, latin=NUM_F)])
tb(s_mkt, xs[2], 4.5, 7.2, 0.5, [dict(t="2024 年全国化妆品类零售额", size=22, color=GREY)])
tb(s_mkt, xs[2], 5.35, 7.2, 1.0, [dict(t="1069亿", size=54, bold=True, color=CORAL, latin=NUM_F)])
tb(s_mkt, xs[2], 6.4, 7.2, 0.5, [dict(t="抗衰护肤品细分市场规模(2024)", size=22, color=GREY)])
tb(s_mkt, xs[2], 7.15, 7.2, 1.5,
[dict(t="高客单、高复购;线上「可解释的决策工具」仍是缺口", size=24, color=GREY)])
# 需求原声
tb(s_mkt, 1.5, 9.55, 10, 0.5, [dict(t="需求原声", size=24, bold=True, color=GREY)])
band(s_mkt, 0, 10.25, 26.67, 2.25)
quotes = ["「不知道自己适不适合这款抗老精华」", "「想先测一下再买,别乱试」", "「报告要能看懂,最好直接告诉我买什么」"]
for i, q in enumerate(quotes):
tb(s_mkt, 1.5 + i * 8.6, 10.25, 8.2, 2.25,
[dict(t=q, size=26, bold=True)], anchor=MSO_ANCHOR.MIDDLE)
tb(s_mkt, 1.5, 12.9, 24, 0.5,
[dict(t="数据来源:国家统计局社会消费品零售总额(化妆品类,2024);智研咨询《2024 年中国抗衰老行业研究》",
size=22, color=GREY)])
# ---- P5 02 方案与价值 ----
drop(find(s_val, "产品价值和商业价值"))
tb(s_val, 1.5, 2.55, 11.5, 0.6, [dict(t="产品价值与商业价值", size=32, bold=True, color=ORANGE)])
vals = [
("检测可信", "AI 图像分析替代主观猜测,输出可解释的检测报告"),
("决策提效", "报告直达适配商品,缩短「浏览—下单」路径,提升转化与客单匹配"),
("服务资产", "历史报告沉淀,支撑复访召回、换季关怀与复购运营"),
]
for i, (lead, desc) in enumerate(vals):
y = 3.45 + i * 1.95
sq = s_val.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(1.5), Inches(y + 0.16), Inches(0.26), Inches(0.26))
sq.fill.solid(); sq.fill.fore_color.rgb = CORAL; sq.line.fill.background(); sq.shadow.inherit = False
tb(s_val, 2.1, y, 10.6, 1.7,
[dict(t=lead, size=30, bold=True, sa=6), dict(t=desc, size=24, color=GREY)])
tb(s_val, 14.2, 2.55, 11, 0.6, [dict(t="适用行业与运用场景", size=32, bold=True, color=ORANGE)])
chips = [("护肤", 14.2, 3.5), ("功效型美妆", 19.3, 3.5), ("抗老精华", 14.2, 4.55), ("品牌会员中心", 19.3, 4.55)]
for t, cx, cy in chips:
sp = card(s_val, cx, cy, 4.7, 0.85, fill=WHITE, line=CORAL)
tf = sp.text_frame
tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = 0
tf.vertical_anchor = MSO_ANCHOR.MIDDLE
p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER
r = p.add_run(); r.text = t
set_font(r, 26, False, CORAL)
scenes = ["· 店铺首页服务入口", "· 商品详情页挂载", "· 直播间互动测肤", "· 会员日与换季活动"]
tb(s_val, 14.2, 5.95, 11, 3.4,
[dict(t=s, size=26, sa=14) for s in scenes])
band(s_val, 0, 10.1, 26.67, 2.8)
tb(s_val, 1.5, 10.4, 10, 0.5, [dict(t="商业价值", size=24, bold=True, color=GREY)])
biz = [("提升转化", "把不确定变成可解释的购买理由"),
("优化客单", "按肌龄匹配商品,减少试错退货"),
("沉淀会员", "报告资产驱动复访与复购")]
for i, (t, d) in enumerate(biz):
x = 1.5 + i * 8.6
tb(s_val, x, 11.1, 7.8, 1.5,
[dict(t=t, size=28, bold=True, sa=6), dict(t=d, size=22, color=GREY)])
# ---- P6 02 产品核心功能与链路(配截图) ----
replace_text(find(feat, "小程序方案及价值"), "产品核心功能与链路", widen=9.5)
drop(find(feat, "产品价值和商业价值"))
feats = [
("01", "授权登录", "淘宝账号授权,协议确认后进入服务"),
("02", "基础档案", "出生年份、地区、肤质,一次填写本地缓存"),
("03", "三步拍摄", "正脸 / 左脸 / 右脸拍摄或相册选图"),
("04", "AI 检测报告", "综合评分、肌龄、七项分项与三维指标"),
("05", "护理与推荐", "护理关注选三,按肌龄推荐适配精华"),
("06", "历史报告", "最近多份完整报告,可回看对比"),
]
for i, (n, t, d) in enumerate(feats):
y = 2.35 + i * 1.12
circle(feat, 1.5, y + 0.06, 0.6, n, size=22)
tb(feat, 2.4, y, 9.9, 1.05,
[dict(t=t, size=28, bold=True, sa=3), dict(t=d, size=22, color=GREY)])
phones = [("phone-profile.png", "基础档案"), ("phone-camera.png", "正面拍摄引导"), ("phone-report.png", "AI 检测报告")]
for i, (f, cap) in enumerate(phones):
x = 13.3 + i * 3.85
pic(feat, f"{AST}\\{f}", x, 2.3, 3.3, 6.48)
tb(feat, x, 8.95, 3.3, 0.5, [dict(t=cap, size=24, color=GREY, align=PP_ALIGN.CENTER)])
steps = ["启动授权", "档案填写", "三步拍摄", "生成报告", "护理推荐", "商品转化"]
for i, st in enumerate(steps):
x = 2.1 + i * 3.9
circle(feat, x + 1.1, 10.3, 0.8, str(i + 1), size=24)
tb(feat, x, 11.3, 3.0, 0.5, [dict(t=st, size=24, bold=True, align=PP_ALIGN.CENTER)])
if i < len(steps) - 1:
tb(feat, x + 2.95, 10.42, 0.9, 0.6,
[dict(t="→", size=30, bold=True, color=GREY, align=PP_ALIGN.CENTER)])
tb(feat, 1.5, 12.35, 24.5, 1.6,
[dict(t="七项分析:轮廓紧致 · 皱纹细纹 · 肤色色素 · 平滑细腻 · 水分水润 · 敏感泛红 · 痘痘皮损", size=22, color=GREY, sa=6),
dict(t="三维指标:紧塑力 / 活力感 / 稳定力,支撑综合评分与展示肌龄", size=22, color=GREY, sa=6),
dict(t="推荐逻辑:按展示肌龄分档匹配精华组别,报告页可跳转商品详情", size=22, color=GREY)])
# ---- P7 02 与平台现有能力的差异与互补 ----
replace_text(find(diff, "小程序方案及价值"), "与平台现有能力的差异与互补", widen=10.5)
drop(find(diff, "产品价值和商业价值"))
heads = [("对比维度", 1.4, 4.6), ("平台现有货架与内容", 6.2, 9.8), ("本小程序互补价值", 16.2, 9.8)]
for t, x, w in heads:
tb(diff, x, 2.7, w, 0.6, [dict(t=t, size=28, bold=True, color=CORAL)])
table = [
("决策依据", "以图文、评价与直播讲解为主", "基于本人面部数据生成可解释报告"),
("选品方式", "用户自行对比商品与参数", "按肌龄与肤质状态主动匹配商品"),
("服务形态", "偏一次性浏览与成交", "报告沉淀可回看,支撑复访复购"),
("商家价值", "流量与成交工具丰富", "新增「专业测肤」信任入口,提升转化"),
]
for i, (lab, a, b) in enumerate(table):
y = 3.85 + i * 2.1
tb(diff, 1.4, y + 0.55, 4.6, 0.7, [dict(t=lab, size=28, bold=True)])
for txt, x in ((a, 6.2), (b, 16.2)):
card(diff, x, y, 9.8, 1.9, fill=CARD if x == 6.2 else WHITE,
line=LGREY if x == 6.2 else CORAL)
tb(diff, x + 0.45, y + 0.35, 8.9, 1.3,
[dict(t=txt, size=26, color=GREY if x == 6.2 else INK)],
anchor=MSO_ANCHOR.MIDDLE)
tb(diff, 1.4, 12.65, 24, 0.8,
[dict(t="「不做货架的替代,补足『专业解释 → 信任 → 决策』的服务缺口」", size=30, bold=True, color=ORANGE)])
# ---- P8 03 商业化运营规划及目标 ----
replace_text(find(s_biz, "商业化运营规划及目标"), "商业化运营规划及目标")
drop(find(s_biz, "目标覆盖商家数"))
tb(s_biz, 2.75, 1.78, 14, 0.5, [dict(t="自研类型申请 · 西安巨子生物 · 首期服务自有旗舰店", size=22, color=GREY)])
tb(s_biz, 1.5, 2.6, 10, 0.6, [dict(t="目标覆盖", size=32, bold=True, color=ORANGE)])
goals = [
("首期(0–6 个月)", "服务自有淘宝旗舰店与核心精华线,验证「测肤—推荐—购买」闭环"),
("扩展(6 个月以上)", "复制至品牌矩阵店铺,沉淀为会员中心的标准服务能力"),
]
for i, (t, d) in enumerate(goals):
x = 1.5 + i * 12.22
card(s_biz, x, 3.4, 11.45, 1.85)
tb(s_biz, x + 0.45, 3.7, 10.6, 1.35,
[dict(t=t, size=28, bold=True, sa=5), dict(t=d, size=24, color=GREY)])
tb(s_biz, 1.5, 5.65, 10, 0.6, [dict(t="上线后的运营节奏", size=32, bold=True, color=ORANGE)])
phases = [
("上线期(0–1 月)", "店铺首页与详情页入口曝光;引导完成首测;紧盯拍摄成功率与报告完成率。"),
("成长期(1–3 月)", "大促与直播挂载测肤互动;会员专属复测提醒;持续优化推荐与话术转化。"),
("沉淀期(3 月以上)", "历史报告召回与换季护理活动;A/B 优化推荐规则;提升复测率与连带购买。"),
]
for i, (t, d) in enumerate(phases):
x = 1.5 + i * 8.13
card(s_biz, x, 6.45, 7.4, 3.2)
tb(s_biz, x + 0.45, 6.8, 6.5, 2.5,
[dict(t=t, size=28, bold=True, sa=8), dict(t=d, size=24, color=GREY)])
tb(s_biz, 1.5, 10.1, 10, 0.6, [dict(t="关键指标", size=32, bold=True, color=ORANGE)])
kpis = [
("体验", "拍摄完成率 · 报告完成率 · 报告回访时长"),
("转化", "推荐点击率 · 详情跳转率 · 测肤后下单转化"),
("运营", "复测率 · 连带购买 · 会员渗透"),
("合规", "数据按账号隔离 · 权限按需申请 · 不作医疗诊断承诺"),
]
for i, (t, d) in enumerate(kpis):
x = 1.5 + i * 5.99
card(s_biz, x, 10.9, 5.71, 2.35)
tb(s_biz, x + 0.4, 11.2, 4.95, 1.8,
[dict(t=t, size=28, bold=True, sa=6), dict(t=d, size=22, color=GREY)])
# ---- P9 致谢 ----
tb(s_thx, 4.85, 8.35, 16, 0.6,
[dict(t="西安巨子生物基因技术股份有限公司", size=26, color=WHITE)])
tb(s_thx, 4.85, 9.2, 16, 0.5,
[dict(t="AI 智能测肤 · 淘宝小程序产品方案", size=22, color=WHITE)])
prs.save(OUT)
print("saved:", OUT, "slides:", len(prs.slides))
if __name__ == "__main__":
build()