chore: [workflow] 提交工作区改动

This commit is contained in:
wangyunlong committed 2026-09-07 15:14:54 +08:00
1 parent 485e77ee74
commit 2ff588a65d
27 files changed
+373 -138

No files matched your search

@@ -43,6 +43,7 @@ TARGET_URLS = [
"https://www.chanmama.com/bloggerRank/ujU3Wwa9udJc59DKO5dAA37ILJPsWvyg.html?activeTab=aweme",
"https://www.chanmama.com/bloggerRank/ulaJWIvluqa_q6bQZzLdkzmLXNz64tgg.html?activeTab=aweme",
]
CHANMAMA_HOME_URL = "https://www.chanmama.com/"
DOWNLOAD_DIR = str(PATHS.raw_root / "chanmama")
COOKIE_FILE = str(PATHS.browser_cookie_file)
COOKIE_MAX_AGE = 7 * 24 * 3600 # cookie 有效期 7 天(秒)
@@ -486,7 +487,17 @@ def navigate_to_target(driver, url):
if attempt >= CHANMAMA_NAVIGATION_ATTEMPTS:
break
print(f"⚠️ 目标页未完成渲染,重新打开一次: {exc}")
time.sleep(2)
# A same-URL reload can preserve the half-mounted SPA state. Go
# through the site root once so the next target gets a fresh app
# mount while retaining the authenticated browser session.
try:
driver.get(CHANMAMA_HOME_URL)
WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
except Exception as reset_exc:
print(f"⚠️ 蝉妈妈页面重置失败,继续重试目标页: {reset_exc}")
time.sleep(1)
raise RuntimeError(f"蝉妈妈目标页加载失败: {url}; {last_error}") from last_error
@@ -536,6 +547,16 @@ _VIDEO_LIST_READY_SCRIPT = r"""
&& style.opacity !== '0' && rect.width > 0 && rect.height > 0;
};
const body = (document.body?.innerText || '').replace(/\s+/g, ' ');
// Current Chanmama renders the list under aweme-record-wrapper before the
// virtual table has stable row geometry. The page is usable once this
// visible root exposes the list/export semantics, even if the table rows
// are still being measured.
if (/视频记录|导出数据/.test(body)
&& [...document.querySelectorAll(
'.aweme-record-wrapper,.list-auth-wrap-block,.list-box'
)].some((el) => visible(el))) {
return true;
}
if (/预估曝光|曝光量/.test(body)
&& [...document.querySelectorAll('table,[role="row"],[class*="video"],[class*="list"],[class*="record"]')]
.some((el) => visible(el) && /预估曝光|曝光量|播放量/.test(el.innerText || ''))) {
@@ -3,6 +3,8 @@ ALTER TABLE cmt_styles
ADD COLUMN IF NOT EXISTS brand VARCHAR(128) DEFAULT '',
ADD COLUMN IF NOT EXISTS erp_style_codes TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS platform_product_ids JSONB DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS big_nfc_count INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS small_nfc_count INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS product_type VARCHAR(128) DEFAULT '',
ADD COLUMN IF NOT EXISTS dimensions VARCHAR(255) DEFAULT '',
ADD COLUMN IF NOT EXISTS colors TEXT[] DEFAULT '{}',
@@ -17,6 +19,8 @@ ALTER TABLE cmt_styles
ADD COLUMN IF NOT EXISTS product_researched_at TIMESTAMPTZ;
COMMENT ON COLUMN cmt_styles.platform_product_ids IS 'Product IDs grouped by Feishu platform';
COMMENT ON COLUMN cmt_styles.big_nfc_count IS 'Large NFC product count maintained by product lifecycle';
COMMENT ON COLUMN cmt_styles.small_nfc_count IS 'Small NFC product count maintained by product lifecycle';
COMMENT ON COLUMN cmt_styles.product_source_urls IS 'Public pages used to verify product facts';
COMMENT ON COLUMN cmt_styles.product_research_status IS 'verified, partial, or pending';
@@ -11,6 +11,8 @@ CREATE TABLE IF NOT EXISTS cmt_styles (
brand VARCHAR(128) DEFAULT '',
erp_style_codes TEXT[] DEFAULT '{}',
platform_product_ids JSONB DEFAULT '{}'::jsonb,
big_nfc_count INTEGER NOT NULL DEFAULT 1,
small_nfc_count INTEGER NOT NULL DEFAULT 1,
product_type VARCHAR(128) DEFAULT '',
dimensions VARCHAR(255) DEFAULT '',
colors TEXT[] DEFAULT '{}',
@@ -35,6 +37,9 @@ COMMENT ON TABLE cmt_styles IS '款式表';
COMMENT ON COLUMN cmt_styles.name IS '款式名称';
COMMENT ON COLUMN cmt_styles.style_category IS '款式风格';
COMMENT ON COLUMN cmt_styles.platform IS '平台分组,如天猫';
COMMENT ON COLUMN cmt_styles.platform_product_ids IS '产品生命进程统一维护的平台商品编码,固定键 tm/jd/jd_self/dy';
COMMENT ON COLUMN cmt_styles.big_nfc_count IS '大 NFC 商品数量';
COMMENT ON COLUMN cmt_styles.small_nfc_count IS '小 NFC 商品数量';
CREATE INDEX IF NOT EXISTS idx_cmt_styles_platform ON cmt_styles (platform);
@@ -90,6 +90,7 @@ BUYIN_MARKET_URL = (
"&btm_show_id=a1499f13-89eb-4bc0-99f5-ceb3b8f2e600"
)
BUYIN_DETAIL_URL_MARK = "/dashboard/servicehall/daren-profile"
BUYIN_SEARCH_INPUT_SELECTOR = "input[placeholder*='搜达人昵称']"
# --buyin 仍可显式强制使用抖店达人广场;日报默认保留星图主采集,未命中
# 的任务由主入口在同一批次内切换到抖店补采。
@@ -755,17 +756,19 @@ def dismiss_popups(page: Page) -> None:
def select_nickname_search(page: Page) -> None:
if CONTENT_SOURCE == "buyin":
if not click_by_text(page, "找达人", exact=True, timeout=5000):
page.evaluate(
"""
() => {
const target = [...document.querySelectorAll('button, a, div, span')]
.find((el) => (el.innerText || '').trim() === '找达人');
target?.click();
}
"""
# Buyin has a global "找达人" navigation item. Clicking the first
# text match can leave the authenticated market and land on
# www.douyinec.com, where there is no creator-search input at all.
# The market route already owns the search box, so target the box
# directly and repair the route when a stale page is still open.
input_locator = page.locator(f"{BUYIN_SEARCH_INPUT_SELECTOR}:visible").first
if not input_locator.count():
current_path = urlparse(str(getattr(page, "url", "") or "")).path.rstrip(
"/"
)
page.wait_for_timeout(800)
if current_path != "/dashboard/servicehall/daren-square":
page.goto(MARKET_URL, wait_until="domcontentloaded", timeout=30000)
input_locator.wait_for(state="visible", timeout=15000)
return
if not click_by_text(page, "昵称找人", exact=True, timeout=5000):
@@ -805,7 +808,12 @@ def search_creator(page: Page, creator_name: str, creator_id: str | None = None)
select_nickname_search(page)
# Keep the platform's original input locator. The query value is the
# only thing that changes between the ID attempt and nickname fallback.
input_locator = page.locator("input:visible").first
input_selector = (
f"{BUYIN_SEARCH_INPUT_SELECTOR}:visible"
if CONTENT_SOURCE == "buyin"
else "input:visible"
)
input_locator = page.locator(input_selector).first
input_locator.wait_for(state="visible", timeout=30000)
input_locator.click()
previous_result_signature = (
@@ -21,11 +21,10 @@ PRODUCT_SCHEDULED_WORKFLOW_IDS = (
"product.erp_all_shop_daily",
"product.market_rank",
"product.tmall_baibu_apply",
)
PRODUCT_MANUAL_WORKFLOW_IDS = (
"product.video_upload",
"product.jd_video_upload",
)
PRODUCT_MANUAL_WORKFLOW_IDS = ()
PRODUCT_WORKFLOW_IDS = PRODUCT_SCHEDULED_WORKFLOW_IDS + PRODUCT_MANUAL_WORKFLOW_IDS
PRODUCT_TIMEOUT_SECONDS = 6 * 60 * 60
PRODUCT_RESOURCE = "module:product_commerce"
@@ -23,7 +23,7 @@ def safe_name(value):
def load_style_meta():
"""优先从飞书读取 ERP 编码,失败则回退 styles_input.json。"""
"""只从产品生命进程读取 ERP 编码;不使用本地旧配置回退。"""
result = {}
try:
for item in get_erp_styles_input():
@@ -35,19 +35,7 @@ def load_style_meta():
if result:
return result
except Exception as exc:
print(f"[WARN] 动态读取 ERP 编码失败,使用本地文件回退: {exc}")
if not STYLE_INPUT.exists():
return {}
items = read_json(STYLE_INPUT)
for item in items:
style = item.get("style_name") or item.get("款式") or item.get("style")
if not style:
continue
codes = item.get("erp_style_codes") or item.get("erp款式编码") or item.get("codes") or []
if isinstance(codes, str):
codes = [x.strip() for x in codes.split(",") if x.strip()]
result[style] = {"erp_style_codes": codes}
raise RuntimeError("产品生命进程身份源不可用,拒绝读取 styles_input.json") from exc
return result
@@ -49,49 +49,15 @@ from jd_peer_product_data_collector import ( # noqa: E402
INVENTORY_URL = "https://ppzh.jd.com/inventoryweb/brand/view/supplychainAnalysis/inventoryDetail.html"
DEFAULT_OUTPUT_DIR = RAW_DATA_ROOT / "jd自营库存销量数据"
# 保留硬编码作为灾备回退;运行时优先从飞书多维表格读取。
_STATIC_STYLE_SKU_GROUPS = [
{"style_name": "盖亚斜挎", "skus": ["100044949353", "100087299891"]},
{"style_name": "极星pro", "skus": ["100117005088", "100100271235"]},
{"style_name": "盖亚微单", "skus": ["100108144308", "100108144324"]},
{"style_name": "盖世m1", "skus": ["100191634938"]},
{
"style_name": "星迹2",
"skus": [
"100108144306",
"100150785348",
"100108144260",
"100100271371",
"100241562067",
"100241562047",
],
},
{"style_name": "星云2", "skus": ["100160619963", "100160619965", "100248422862"]},
{
"style_name": "宙斯",
"skus": ["100221530889", "100306128160", "100306128166", "100255125975", "100239269869"],
},
{"style_name": "阿波罗x1", "skus": ["100058323820", "100058323830", "100058916832", "100058916818"]},
{"style_name": "逐星", "skus": ["100165451136", "100165451142"]},
{"style_name": "星云mini", "skus": ["100215176791", "100215176793"]},
{"style_name": "晨星2", "skus": ["100287829888", "100287829866"]},
{"style_name": "极星双肩", "skus": ["100247532920", "100247532928"]},
{"style_name": "凌云air", "skus": ["100291530148", "100218450063"]},
{"style_name": "觅光", "skus": ["100241563697"]},
{"style_name": "云卷2", "skus": ["100329997886"]},
{"style_name": "拓界mode", "skus": ["100251533359", "100251533369"]},
]
def _get_style_sku_groups():
"""优先从飞书读取京东自营 SKU 分组,失败则回退硬编码。"""
"""从产品生命进程读取完整身份的京东自营 SKU 分组,失败则不采集。"""
try:
groups = get_jd_self_sku_groups()
if groups:
return groups
except Exception as exc:
print(f"[WARN] 动态读取京东自营 SKU 配置失败,使用硬编码回退: {exc}")
return _STATIC_STYLE_SKU_GROUPS
print(f"[FAIL] 产品生命进程身份源不可用,跳过京东自营采集: {exc}")
return []
STYLE_SKU_GROUPS = _get_style_sku_groups()
@@ -101,7 +67,7 @@ def _print_preflight() -> None:
"""启动期轻量打印:京东自营周汇总分组里今日待采款式数。"""
try:
styles = get_jd_self_styles()
print(f"[JD_SELF] 飞书表格 周汇总待采 {len(styles)} 款 (仅要求 SKU 非空)")
print(f"[JD_SELF] 产品生命进程 周汇总待采 {len(styles)} 款 (要求 ERP + 京东自营 SKU)")
except Exception as exc:
print(f"[WARN] 飞书读取失败, 跳过 preflight: {exc}")
@@ -25,6 +25,7 @@ try:
)
from .config.style_config_loader import (
StyleConfigLoader,
has_complete_style_identity,
)
from .lark_cli_runtime import run_lark_cli
except ImportError: # Direct script execution from the module root.
@@ -36,6 +37,7 @@ except ImportError: # Direct script execution from the module root.
)
from config.style_config_loader import (
StyleConfigLoader,
has_complete_style_identity,
)
from lark_cli_runtime import run_lark_cli
@@ -328,7 +330,7 @@ def plan_erp_styles(
str(code).strip() for code in raw_codes if str(code).strip()
)
)
if not codes:
if not codes or not has_complete_style_identity(style_config):
missing.append(target.product_name)
continue
key = normalized_name(style_name)
@@ -402,7 +404,7 @@ def require_complete_erp_mappings(missing_products: Sequence[str]) -> None:
if not missing:
return
raise RuntimeError(
"以下产品在飞书款式配置中缺少款式映射或 ERP 款式编码,"
"以下产品在产品生命进程中缺少款式映射、ERP 款式编码或平台商品编码,"
"已停止正式写入且目标表未清空: " + "、".join(missing)
)
@@ -169,6 +169,8 @@ _BATCH_FAILURE_RE = re.compile(
)
_DRAFT_COUNT_RE = re.compile(r"草稿\s*[((]\s*(?P<count>\d+)\s*[))]")
_DRAFT_EMPTY_MARKERS = ("暂无数据", "数据为空", "暂无商品", "没有符合条件的商品")
_ANNOUNCEMENT_MARKERS = ("公告", "通知")
_ANNOUNCEMENT_DISMISS_LABELS = ("我已阅读完成", "关闭")
_EDITOR_CLOSE_SELECTORS = (
"button[aria-label='关闭此对话框']",
"[role='button'][aria-label='关闭此对话框']",
@@ -627,6 +629,58 @@ def _visible_locator(context: Any, selectors: Iterable[str]) -> Any | None:
return None
def _dismiss_blocking_announcements(page: Any) -> bool:
"""Close a visible Tmall announcement dialog that blocks page actions.
The old-shop activity page can show an announcement overlay after the
account identity has already loaded. The underlying ``商品批量导入``
button remains present, but a normal click is intercepted by the dialog.
Only dialogs whose own text contains an announcement marker are touched;
business dialogs are left alone.
"""
dismissed = False
for context in _page_contexts(page):
get_by_role = getattr(context, "get_by_role", None)
if not callable(get_by_role):
continue
try:
dialogs = get_by_role("dialog")
count = min(dialogs.count(), 12)
except Exception:
continue
for index in range(count):
dialog = dialogs.nth(index)
try:
if not dialog.is_visible():
continue
dialog_text = _normalized_page_text(
dialog.inner_text(timeout=2_000)
)
except Exception:
continue
if not any(marker in dialog_text for marker in _ANNOUNCEMENT_MARKERS):
continue
candidates: list[Any] = []
for label in _ANNOUNCEMENT_DISMISS_LABELS:
candidates.extend(_visible_text_candidates(dialog, label))
if not candidates:
close = _visible_locator(dialog, _EDITOR_CLOSE_SELECTORS)
if close is not None:
candidates.append(close)
for candidate in candidates:
try:
candidate.click(timeout=8_000)
dismissed = True
break
except Exception:
continue
if dismissed:
return True
return False
def _find_password_login_form(
page: Any,
*,
@@ -860,6 +914,8 @@ def _open_batch_import(
) -> _BatchImportStatus | None:
"""Open the import dialog, failing closed on an active prior operation."""
_dismiss_blocking_announcements(page)
# The account marker is rendered before the 最近操作 panel on this SPA.
# Capture the panel after it has had a short chance to hydrate; otherwise a
# previous terminal summary can appear only after the dialog is opened and
@@ -886,6 +942,7 @@ def _open_batch_import(
deadline = time.monotonic() + 20
while time.monotonic() < deadline:
_dismiss_blocking_announcements(page)
if _click_text(page, ("商品批量导入",)):
return previous
current = _latest_batch_import_status(_page_text(page))
@@ -993,26 +1050,28 @@ def _return_to_draft_list(page: Any, entry: BaibuEntry) -> None:
"""Leave the current editor and wait until the draft list is usable.
Tmall keeps the editor open after ``下一步`` saves a product. The next
product must be selected from the draft list, so explicitly navigate back
before the loop reads the draft count. The visible 草稿 tab is preferred;
closing the editor and browser history are fallbacks for the two page
variants used by the old and new seller consoles.
product must be selected from the draft list, so close the editor first
and only then navigate back before the loop reads the draft count. This
prevents a click on the 草稿 tab or the next 完善商品 action from being
intercepted by the still-open drawer. Browser history remains a fallback
for the two page variants used by the old and new seller consoles.
"""
deadline = time.monotonic() + _DRAFT_NAVIGATION_TIMEOUT_SECONDS
close_attempted = False
back_attempted = False
while time.monotonic() < deadline:
if _click_draft_tab(page) and _draft_list_is_ready(page):
return
if not close_attempted:
close_attempted = True
if _close_editor(page):
# Let the drawer close animation finish before trying the
# tab again or falling back to browser history.
# draft list or falling back to browser history.
time.sleep(0.5)
continue
if _draft_list_is_ready(page):
return
if _click_draft_tab(page) and _draft_list_is_ready(page):
return
if not back_attempted:
back_attempted = True
+5 -5
View File
@@ -1291,14 +1291,14 @@
const platforms = Array.isArray(record.platforms) ? record.platforms : [];
const enabledPlatforms = platforms.filter((platform) => platform.enabled !== false);
const itemCount = enabledPlatforms.reduce((total, platform) => total + (Array.isArray(platform.item_ids) ? platform.item_ids.length : 0), 0);
const platformRows = enabledPlatforms.slice(0, 4).map((platform) => `<span class="config-platform-chip"><b>${esc(platform.platform || "未填写平台")}</b><small>${Array.isArray(platform.item_ids) && platform.item_ids.length ? `${platform.item_ids.length} 个 ID` : "待补 ID"}</small></span>`).join("");
const platformRows = enabledPlatforms.slice(0, 4).map((platform) => `<span class="config-platform-chip"><b>${esc(platform.platform || "未填写平台")}</b><small>${Array.isArray(platform.item_ids) && platform.item_ids.length ? `${platform.item_ids.length} 个兼容 ID` : "由生命进程维护"}</small></span>`).join("");
const morePlatforms = enabledPlatforms.length > 4 ? `<span class="config-platform-chip config-platform-chip--more">+${enabledPlatforms.length - 4}</span>` : "";
const destinationNames = dynamicConfigDestinations(record).filter((item) => item.enabled && item.url).slice(0, 3).map((item) => `<span>${esc(item.label)}</span>`).join("");
const moreTargets = targetCount > 3 ? `<span class="config-target-more">+${targetCount - 3}</span>` : "";
return `<article class="dynamic-config-record" role="listitem" data-config-id="${Number(record.id)}">
<div class="dynamic-config-record__identity" data-label="款式身份"><span class="dynamic-config-record__avatar">${esc(configInitial(style))}</span><div><strong>${esc(style)}</strong><small>${esc(brand)}${record.note ? ` · ${esc(record.note)}` : ""}</small><span class="dynamic-config-record__id">配置 #${Number(record.id)}</span></div></div>
<div class="dynamic-config-record__platforms" data-label="平台商品"><div class="dynamic-config-record__metric"><strong>${enabledPlatforms.length}</strong><span>个平台 · ${itemCount} 个商品 ID</span></div><div class="config-platform-list">${platformRows || '<span class="muted">暂无平台映射</span>'}${morePlatforms}</div></div>
<div class="dynamic-config-record__targets" data-label="ERP / 业务目标"><div class="dynamic-config-record__erp"><span>ERP</span>${configCodeList(record.erp_codes)}</div><div class="config-target-list">${destinationNames || '<span class="muted">尚未配置目标地址</span>'}${moreTargets}</div></div>
<div class="dynamic-config-record__platforms" data-label="平台目标"><div class="dynamic-config-record__metric"><strong>${enabledPlatforms.length}</strong><span>个平台 · ${itemCount} 个兼容 ID</span></div><div class="config-platform-list">${platformRows || '<span class="muted">暂无平台目标</span>'}${morePlatforms}</div></div>
<div class="dynamic-config-record__targets" data-label="业务目标"><div class="dynamic-config-record__erp"><span>身份</span><span class="muted">产品生命进程</span></div><div class="config-target-list">${destinationNames || '<span class="muted">尚未配置目标地址</span>'}${moreTargets}</div></div>
<div class="dynamic-config-record__status" data-label="状态"><span class="config-state-pill config-state-pill--${record.enabled ? "enabled" : "disabled"}">${record.enabled ? "启用" : "停用"}</span><small>${targetCount} 个目标已接入</small></div>
<div class="dynamic-config-record__action"><button class="dynamic-config-edit" type="button" data-action="edit-dynamic-config" data-config-id="${Number(record.id)}">编辑配置<span aria-hidden="true">→</span></button></div>
</article>`;
@@ -1335,9 +1335,9 @@
function dynamicConfigPlatformRow(platform = {}) {
const enabled = platform.enabled !== false;
return `<article class="dynamic-config-platform-row" data-platform-row>
<div class="dynamic-config-platform-row__header"><span class="dynamic-config-platform-row__mark" aria-hidden="true">台</span><div><strong data-platform-preview>${esc(platform.platform || "新平台")}</strong><small>平台级商品身份</small></div><label class="dynamic-config-platform-toggle"><input data-platform-field="enabled" type="checkbox" ${enabled ? "checked" : ""}><span>启用</span></label><button class="dynamic-config-platform-remove" data-action="remove-platform" type="button" aria-label="删除平台">×</button></div>
<div class="dynamic-config-platform-row__header"><span class="dynamic-config-platform-row__mark" aria-hidden="true">台</span><div><strong data-platform-preview>${esc(platform.platform || "新平台")}</strong><small>平台画像与启停</small></div><label class="dynamic-config-platform-toggle"><input data-platform-field="enabled" type="checkbox" ${enabled ? "checked" : ""}><span>启用</span></label><button class="dynamic-config-platform-remove" data-action="remove-platform" type="button" aria-label="删除平台">×</button></div>
<div class="dynamic-config-platform-row__fields"><label class="field"><span>平台名称 <i>必填</i></span><input data-platform-field="platform" list="config-platform-options" maxlength="80" value="${esc(platform.platform || "")}" placeholder="例如:天猫" required></label>
<label class="field"><span>商品 ID / SPU / SKU <small>多个用逗号或换行分隔</small></span><textarea data-platform-field="item_ids" rows="2" placeholder="例如:1074059145982">${esc((platform.item_ids || []).join(", "))}</textarea></label>
<label class="field"><span>商品 ID / SPU / SKU <small>只读,前往产品生命进程维护</small></span><textarea data-platform-field="item_ids" rows="2" placeholder="由产品生命进程提供" readonly>${esc((platform.item_ids || []).join(", "))}</textarea></label>
<label class="field field--full"><span>本平台人群画像表 <small>不采集画像可留空</small></span><input data-platform-field="persona_bitable_url" type="url" value="${esc(platform.persona_bitable_url || "")}" placeholder="https://…"></label></div>
</article>`;
}
+10 -10
View File
@@ -74,7 +74,7 @@
</button>
<button class="module-nav__item sidebar-config__item" id="dynamic-config-nav-button" type="button" data-view="dynamic-configs" aria-controls="dynamic-config-view">
<span class="module-nav__glyph sidebar-config__glyph dynamic-config-nav__glyph" aria-hidden="true">配</span>
<span class="module-nav__copy"><strong>款式平台配置</strong><small>ERP、平台商品 ID 与目标表</small></span>
<span class="module-nav__copy"><strong>款式目标配置</strong><small>业务目标、画像表与启停</small></span>
<span class="module-nav__count" id="dynamic-config-nav-count">…</span>
</button>
</div>
@@ -351,7 +351,7 @@
<div class="dynamic-config-header__copy">
<span class="eyebrow">STYLE CONFIGURATION WORKSPACE</span>
<h1 id="dynamic-config-title">款式配置中心</h1>
<p>一款一条配置,集中维护 ERP、飞书业务目标和各平台商品身份。</p>
<p>一款一条配置,集中维护飞书业务目标、平台画像和启停;商品身份请在产品生命进程维护。</p>
</div>
<div class="dynamic-config-header__actions">
<div class="dynamic-config-source-note"><span class="config-source-dot" aria-hidden="true"></span><span><small>配置来源</small><strong>云端 PostgreSQL</strong></span></div>
@@ -362,7 +362,7 @@
<section class="dynamic-config-source-bar" aria-label="款式配置读取范围">
<div class="dynamic-config-source-bar__lead"><span class="config-source-icon" aria-hidden="true">↗</span><div><strong>统一配置,按款式生效</strong><p>新启动的工作流会读取这里的最新版本。</p></div></div>
<div class="dynamic-config-source-bar__items"><span><b>01</b>ERP 款式编码</span><span><b>02</b>业务目标地址</span><span><b>03</b>平台商品身份</span></div>
<div class="dynamic-config-source-bar__items"><span><b>01</b>业务目标地址</span><span><b>02</b>平台画像表</span><span><b>03</b>启停状态</span></div>
</section>
<section class="dynamic-config-summary" aria-label="配置统计">
@@ -386,7 +386,7 @@
<div class="dynamic-config-feedback" id="dynamic-config-feedback" role="status" aria-live="polite" hidden></div>
<div class="dynamic-config-table-wrap">
<div class="dynamic-config-table" role="list" aria-label="款式配置列表">
<div class="dynamic-config-table-head" aria-hidden="true"><span>款式身份</span><span>平台商品</span><span>ERP / 业务目标</span><span>状态</span><span>操作</span></div>
<div class="dynamic-config-table-head" aria-hidden="true"><span>款式身份</span><span>平台目标</span><span>业务目标</span><span>状态</span><span>操作</span></div>
<div id="dynamic-config-table-body"><div class="dynamic-config-loading">正在读取云端配置…</div></div>
</div>
</div>
@@ -560,7 +560,7 @@
<div class="overlay__backdrop" data-close="dynamic-config" aria-hidden="true"></div>
<section class="modal dynamic-config-modal" id="dynamic-config-modal" role="dialog" aria-modal="true" aria-labelledby="dynamic-config-form-title" aria-hidden="true">
<header class="modal__header dynamic-config-modal__header">
<div class="dynamic-config-modal__heading"><div class="dynamic-config-modal__eyebrow"><span class="config-modal-mark" aria-hidden="true">款</span><span class="eyebrow">STYLE CONFIGURATION</span></div><h2 id="dynamic-config-form-title">新增款式</h2><p id="dynamic-config-form-subtitle">一次维护款式公共配置与全部平台商品身份。</p></div>
<div class="dynamic-config-modal__heading"><div class="dynamic-config-modal__eyebrow"><span class="config-modal-mark" aria-hidden="true">款</span><span class="eyebrow">STYLE CONFIGURATION</span></div><h2 id="dynamic-config-form-title">新增目标配置</h2><p id="dynamic-config-form-subtitle">身份在产品生命进程维护;这里配置业务目标与平台画像。</p></div>
<div class="dynamic-config-modal__header-actions"><span class="dynamic-config-revision" id="dynamic-config-editor-revision">新配置</span><button class="icon-button" id="dynamic-config-form-close" type="button" aria-label="关闭配置编辑">×</button></div>
</header>
<form id="dynamic-config-form" novalidate>
@@ -569,20 +569,20 @@
<aside class="dynamic-config-form-rail" aria-label="编辑页导航">
<div class="dynamic-config-form-identity"><span class="dynamic-config-editor-avatar" id="dynamic-config-editor-avatar">款</span><div><small>当前款式</small><strong id="dynamic-config-editor-style">新款式</strong><span class="dynamic-config-form-identity__status" id="dynamic-config-editor-status">准备创建</span><span class="dynamic-config-form-identity__summary" id="dynamic-config-editor-summary">新配置 · 1 个平台 · 1 个目标</span></div></div>
<nav class="dynamic-config-form-nav" aria-label="配置分区">
<button type="button" data-config-section-nav="identity" class="is-active"><span>01</span><strong>基础信息</strong><small>款式与 ERP 编码</small></button>
<button type="button" data-config-section-nav="identity" class="is-active"><span>01</span><strong>基础信息</strong><small>款式与状态</small></button>
<button type="button" data-config-section-nav="destinations"><span>02</span><strong>业务目标</strong><small>可扩展飞书表格地址</small></button>
<button type="button" data-config-section-nav="platforms"><span>03</span><strong>平台映射</strong><small>商品 ID 与画像表</small></button>
<button type="button" data-config-section-nav="platforms"><span>03</span><strong>平台目标</strong><small>画像表与启停</small></button>
</nav>
<div class="dynamic-config-form-rail__tip"><span aria-hidden="true">✦</span><div><strong>为下一条逻辑留位置</strong><p>新分析逻辑只需新增一条业务目标,使用稳定的逻辑标识读取对应地址。</p></div></div>
</aside>
<div class="modal__body dynamic-config-form-body">
<p class="form-error" id="dynamic-config-form-error" role="alert" hidden></p>
<section class="dynamic-config-form-section" id="dynamic-config-identity-section" data-config-section="identity" aria-labelledby="dynamic-config-identity-title">
<header><span>01</span><div><h3 id="dynamic-config-identity-title">款式基本信息</h3><p>ERP 编码和款式身份只维护一次,所有平台共享。</p></div></header>
<header><span>01</span><div><h3 id="dynamic-config-identity-title">款式基本信息</h3><p>款式身份只在产品生命进程维护;这里仅关联目标配置。</p></div></header>
<div class="dynamic-config-form-grid">
<label class="field"><span>款式名称 <i>必填</i></span><input id="config-style-name" maxlength="120" placeholder="例如:云栖" required></label>
<label class="field"><span>品牌</span><input id="config-brand" maxlength="120" placeholder="例如:光影行星"></label>
<label class="field field--full"><span>ERP 款式编码 <small>多个编码用逗号或换行分隔</small></span><textarea id="config-erp-codes" rows="2" placeholder="例如:10521, 10522"></textarea></label>
<label class="field field--full"><span>ERP 款式编码 <small>只读,前往产品生命进程维护</small></span><textarea id="config-erp-codes" rows="2" placeholder="由产品生命进程提供" readonly></textarea></label>
<label class="field dynamic-config-enabled-field"><span><strong>参与工作流</strong><small>停用后保留记录,但整款不再被工作流解析。</small></span><input id="config-enabled" type="checkbox" checked><i aria-hidden="true"></i></label>
<label class="field field--full"><span>备注</span><input id="config-note" maxlength="500" placeholder="记录这款的业务说明、特殊规则或负责人"></label>
<input id="config-style-content" type="hidden">
@@ -596,7 +596,7 @@
</section>
<section class="dynamic-config-form-section dynamic-config-platform-section" id="dynamic-config-platforms-section" data-config-section="platforms" aria-labelledby="dynamic-config-platforms-title">
<header><span>03</span><div><h3 id="dynamic-config-platforms-title">平台商品身份</h3><p>一个平台一行:填写商品 ID;需要画像采集时再填本平台人群画像表。</p></div><button class="button button--secondary" id="dynamic-config-add-platform" type="button"><span aria-hidden="true">+</span>添加平台</button></header>
<header><span>03</span><div><h3 id="dynamic-config-platforms-title">平台目标</h3><p>商品编码由产品生命进程提供;这里维护平台画像表和启停。</p></div><button class="button button--secondary" id="dynamic-config-add-platform" type="button"><span aria-hidden="true">+</span>添加平台</button></header>
<div class="dynamic-config-platform-editor" id="dynamic-config-platform-editor"></div>
<datalist id="config-platform-options"><option value="天猫"><option value="京东旗舰店"><option value="京东自营"><option value="抖音"><option value="拼多多"><option value="拼多多光影行星GYXX箱包专卖店"><option value="拼多多光影行星箱包官方旗舰店"><option value="拼多多光影行星官方旗舰店"></datalist>
</section>