style: refine market toolbar layout
This commit is contained in:
parent
ccfb8f14fe
commit
4b5515f6ec
@ -94,4 +94,4 @@ After this one-time bridge upgrade, future updates should continue using the sam
|
|||||||
|
|
||||||
- Keep `.local/extension-key.pem` private and backed up internally.
|
- Keep `.local/extension-key.pem` private and backed up internally.
|
||||||
- Do not commit or share the private key with people who only need to install the extension.
|
- Do not commit or share the private key with people who only need to install the extension.
|
||||||
- If the batch submit backend changes away from `192.168.31.21:8083`, update `scripts/manifest.mjs` before packaging.
|
- If the batch submit backend changes away from `localhost:8083`, update `scripts/manifest.mjs` before packaging.
|
||||||
|
|||||||
685
docs/prototypes/market-toolbar-redesign-preview.html
Normal file
685
docs/prototypes/market-toolbar-redesign-preview.html
Normal file
@ -0,0 +1,685 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>星图插件工具栏改版样例</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--page: #f5f7fa;
|
||||||
|
--panel: #ffffff;
|
||||||
|
--line: #e7ebf0;
|
||||||
|
--text: #20242a;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--soft: #f8fafc;
|
||||||
|
--brand: #ff2f6d;
|
||||||
|
--brand-dark: #85172d;
|
||||||
|
--brand-soft: #fff0f5;
|
||||||
|
--blue: #2563eb;
|
||||||
|
--green: #0f8a5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 1180px;
|
||||||
|
background: var(--page);
|
||||||
|
color: var(--text);
|
||||||
|
font-family:
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
height: 58px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 0 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 23px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 34px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, #ff245f 0 38%, transparent 39%),
|
||||||
|
linear-gradient(45deg, #27c7f2 0 50%, #3664ff 51%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
color: #2f3540;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav .active {
|
||||||
|
color: var(--brand);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav .active::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
bottom: -17px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-btn,
|
||||||
|
.pink-btn {
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 1px solid #d8dee8;
|
||||||
|
background: #fff;
|
||||||
|
color: #303744;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pink-btn {
|
||||||
|
border-color: var(--brand);
|
||||||
|
background: var(--brand);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding: 18px 24px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
height: 46px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #fff;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbar,
|
||||||
|
.filters,
|
||||||
|
.results {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbar {
|
||||||
|
height: 72px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 0 18px;
|
||||||
|
background: var(--brand-soft);
|
||||||
|
color: var(--brand);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: var(--brand);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 440px;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #ff8ab2;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 108px 1fr;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-side {
|
||||||
|
background: #fbfcfe;
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-side div {
|
||||||
|
height: 72px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-main {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
min-height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-title {
|
||||||
|
width: 72px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip,
|
||||||
|
.soft-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.soft-chip {
|
||||||
|
background: var(--brand-soft);
|
||||||
|
color: var(--brand);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
color: #414854;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 18px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count strong {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-shell {
|
||||||
|
margin: 18px 24px 0;
|
||||||
|
border: 1px solid #dfe5ee;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: linear-gradient(180deg, #fff, #fbfcff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-cluster {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding-right: 16px;
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid #7b1a2d;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 14px;
|
||||||
|
background: #7b1a2d;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 800;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-cluster {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #cfe0ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select,
|
||||||
|
.input {
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid #d4dbe6;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #202938;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
min-width: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px 14px;
|
||||||
|
background: #fbfcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #cfe0ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-rule {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #d9efe7;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f0fbf6;
|
||||||
|
color: var(--green);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-rule strong {
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-rule small {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-grid {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.and {
|
||||||
|
color: #0f8a5f;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid #dbe2ec;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric span,
|
||||||
|
.metric b {
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric b {
|
||||||
|
color: var(--green);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric input {
|
||||||
|
min-width: 0;
|
||||||
|
width: 58px;
|
||||||
|
height: 26px;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 16px;
|
||||||
|
border-left: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-btn {
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid #d8dee8;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #303744;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
height: 46px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
color: #5d6675;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #f4b66b, #7660d6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1380px) {
|
||||||
|
.metric-grid {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand"><span class="logo"></span>巨量星图</div>
|
||||||
|
<nav class="nav">
|
||||||
|
<span>首页</span>
|
||||||
|
<span>我的星图</span>
|
||||||
|
<span>找灵感</span>
|
||||||
|
<span class="active">找达人</span>
|
||||||
|
<span>找活动</span>
|
||||||
|
<span>助投放</span>
|
||||||
|
</nav>
|
||||||
|
<div class="top-actions">
|
||||||
|
<button class="small-btn">达人清单</button>
|
||||||
|
<button class="pink-btn">+ 发布任务</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="page">
|
||||||
|
<section class="banner">「精选品牌伙伴计划」优质达人合作推荐</section>
|
||||||
|
|
||||||
|
<section class="searchbar">
|
||||||
|
<button class="tab">内容找人</button>
|
||||||
|
<button class="tab active">昵称找人</button>
|
||||||
|
<input class="search-input" value="抖音 输入达人昵称、抖音号或星图ID" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="filters">
|
||||||
|
<aside class="filter-side">
|
||||||
|
<div>合作诉求</div>
|
||||||
|
<div>匹配度</div>
|
||||||
|
<div>性价比</div>
|
||||||
|
<div>主题推荐</div>
|
||||||
|
</aside>
|
||||||
|
<div class="filter-main">
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-title">合作对象</span>
|
||||||
|
<span class="soft-chip">不限</span>
|
||||||
|
<span class="chip">明星</span>
|
||||||
|
<span class="soft-chip">短视频达人</span>
|
||||||
|
<span class="chip">短剧演员</span>
|
||||||
|
<span class="chip">短直达人</span>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-title">适配行业</span>
|
||||||
|
<span class="chip">不限</span>
|
||||||
|
<span class="soft-chip">品牌曝光</span>
|
||||||
|
<span class="chip">破圈种草</span>
|
||||||
|
<span class="chip">行动转化</span>
|
||||||
|
<span class="chip">品牌5A</span>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-title">达人类型</span>
|
||||||
|
<span class="soft-chip">不限</span>
|
||||||
|
<span class="chip">美妆</span>
|
||||||
|
<span class="chip">萌宠</span>
|
||||||
|
<span class="chip">测评</span>
|
||||||
|
<span class="chip">旅行</span>
|
||||||
|
<span class="chip">母婴亲子</span>
|
||||||
|
<span class="chip">科技数码</span>
|
||||||
|
<span class="chip">生活家居</span>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-title">内容主题</span>
|
||||||
|
<span class="soft-chip">不限</span>
|
||||||
|
<span class="chip">妆容改造</span>
|
||||||
|
<span class="chip">亲子育儿</span>
|
||||||
|
<span class="chip">精彩生活</span>
|
||||||
|
<span class="chip">手机数码</span>
|
||||||
|
<span class="chip">萌宠养护</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="results">
|
||||||
|
<div class="result-top">
|
||||||
|
<div class="count">找到 <strong>10000+</strong> 个达人</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="toolbar-shell" aria-label="插件工具栏改版样例">
|
||||||
|
<div class="toolbar-head">
|
||||||
|
<div class="action-cluster">
|
||||||
|
<button class="tool-btn">导出选中达人数据</button>
|
||||||
|
<button class="tool-btn">按星图ID导出</button>
|
||||||
|
<button class="tool-btn">选择字段</button>
|
||||||
|
<button class="tool-btn">提交批次</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-cluster">
|
||||||
|
<span class="label">视频口径</span>
|
||||||
|
<select class="select">
|
||||||
|
<option>星图视频</option>
|
||||||
|
<option>个人视频</option>
|
||||||
|
</select>
|
||||||
|
<select class="select">
|
||||||
|
<option>只看指派</option>
|
||||||
|
<option>不限指派</option>
|
||||||
|
</select>
|
||||||
|
<select class="select">
|
||||||
|
<option>排除营销</option>
|
||||||
|
<option>不排除营销</option>
|
||||||
|
</select>
|
||||||
|
<select class="select">
|
||||||
|
<option>近90天</option>
|
||||||
|
<option>近30天</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="status">批次提交成功</span>
|
||||||
|
|
||||||
|
<div class="native-actions">
|
||||||
|
<button class="native-btn">自定义指标</button>
|
||||||
|
<button class="native-btn">导出</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar-body">
|
||||||
|
<div class="metric-title">传播指标筛选</div>
|
||||||
|
<div class="metric-rule">全部满足<strong>AND</strong><small>每项取值 ≥ 输入值</small></div>
|
||||||
|
<div class="metric-grid">
|
||||||
|
<label class="metric"><span>评论</span><b>≥</b><input value="1" /></label>
|
||||||
|
<span class="and">且</span>
|
||||||
|
<label class="metric"><span>时长</span><b>≥</b><input value="5" /></label>
|
||||||
|
<span class="and">且</span>
|
||||||
|
<label class="metric"><span>点赞</span><b>≥</b><input value="10" /></label>
|
||||||
|
<span class="and">且</span>
|
||||||
|
<label class="metric"><span>转发</span><b>≥</b><input value="0" /></label>
|
||||||
|
<span class="and">且</span>
|
||||||
|
<label class="metric"><span>完播率</span><b>≥</b><input value="1" /></label>
|
||||||
|
<span class="and">且</span>
|
||||||
|
<label class="metric"><span>互动率</span><b>≥</b><input value="0.1" /></label>
|
||||||
|
<span class="and">且</span>
|
||||||
|
<label class="metric"><span>播放中位数</span><b>≥</b><input value="1000" /></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><input type="checkbox" /> 全选</th>
|
||||||
|
<th>达人信息</th>
|
||||||
|
<th>代表视频</th>
|
||||||
|
<th>达人类型</th>
|
||||||
|
<th>内容主题</th>
|
||||||
|
<th>粉丝数</th>
|
||||||
|
<th>预期CPM</th>
|
||||||
|
<th>完播率</th>
|
||||||
|
<th>21-60s报价</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><input type="checkbox" /></td>
|
||||||
|
<td>
|
||||||
|
<div class="author">
|
||||||
|
<span class="avatar"></span>
|
||||||
|
<div>
|
||||||
|
<strong>柯铭</strong><br />
|
||||||
|
<span style="color: #7a8493">男 · 北京市 · 抖音精选</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>2 个视频</td>
|
||||||
|
<td>萌宠</td>
|
||||||
|
<td>国内旅行 / 昆虫科普</td>
|
||||||
|
<td>1,471.4w</td>
|
||||||
|
<td>43.5</td>
|
||||||
|
<td>26.2%</td>
|
||||||
|
<td class="price">¥600,000</td>
|
||||||
|
<td><button class="pink-btn">下单</button></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
730
docs/项目流程说明文档.md
Normal file
730
docs/项目流程说明文档.md
Normal file
@ -0,0 +1,730 @@
|
|||||||
|
# 项目流程说明文档
|
||||||
|
|
||||||
|
## 1. 项目用途
|
||||||
|
|
||||||
|
本项目是公司内部使用的 Chrome 插件,用于增强巨量星图达人市场页面的达人筛选、数据导出和批次提交效率。
|
||||||
|
|
||||||
|
它解决的业务问题是:使用者在星图达人市场中筛选达人后,可以直接在页面上补充查看看后搜率、秒思后台指标等数据,并把选中的达人导出为 CSV,或提交为后续业务处理批次。
|
||||||
|
|
||||||
|
项目输入包括:
|
||||||
|
|
||||||
|
- 巨量星图达人市场页面中的达人列表、筛选条件和分页结果;
|
||||||
|
- 使用者在页面上勾选的达人;
|
||||||
|
- 使用者粘贴的达人星图 ID;
|
||||||
|
- 使用者填写的批次名称;
|
||||||
|
- 使用者选择的导出字段和传播指标筛选阈值;
|
||||||
|
- 当前插件登录用户的 Logto 身份和访问令牌。
|
||||||
|
|
||||||
|
项目处理过程包括:
|
||||||
|
|
||||||
|
- 在星图达人市场页面挂载插件工具栏;
|
||||||
|
- 读取当前页面或星图列表接口返回的达人数据;
|
||||||
|
- 根据勾选范围、分页范围、阈值筛选规则确定最终达人集合;
|
||||||
|
- 调用星图接口补充看后搜率、画像、商业能力、传播指标等信息;
|
||||||
|
- 调用公司后端接口补充秒思 api 指标;
|
||||||
|
- 生成 CSV 文件,或组装批次 payload 提交到后端。
|
||||||
|
|
||||||
|
项目输出包括:
|
||||||
|
|
||||||
|
- 页面上新增的数据列和状态提示;
|
||||||
|
- 下载到本地的 CSV 文件;
|
||||||
|
- 提交到后端的达人批次;
|
||||||
|
- 插件弹窗中的登录状态和更新状态。
|
||||||
|
|
||||||
|
项目依赖的外部平台、数据源或服务包括:
|
||||||
|
|
||||||
|
- 巨量星图网页和星图接口;
|
||||||
|
- 公司 Logto 登录系统;
|
||||||
|
- 公司 talent-search 后端服务;
|
||||||
|
- 本地或内网批次提交后端;
|
||||||
|
- COS 上的插件更新清单和安装包。
|
||||||
|
|
||||||
|
主要使用者是 AIGC 部门或相关业务同事。通常在以下场景使用:
|
||||||
|
|
||||||
|
- 在星图市场中筛选达人后,需要快速导出达人数据;
|
||||||
|
- 已知一批星图 ID,需要批量补齐画像、效果预估和内容指标;
|
||||||
|
- 需要把一批达人提交给后续业务系统继续处理;
|
||||||
|
- 需要检查或更新内部插件版本。
|
||||||
|
|
||||||
|
## 2. 整体流程总览
|
||||||
|
|
||||||
|
完整主流程按真实使用顺序如下:
|
||||||
|
|
||||||
|
1. 安装或更新插件;
|
||||||
|
2. 登录插件;
|
||||||
|
3. 打开巨量星图达人市场页面;
|
||||||
|
4. 插件读取登录状态并挂载工具栏;
|
||||||
|
5. 插件读取当前星图达人列表,并补充页面展示指标;
|
||||||
|
6. 使用者选择达人、字段、传播指标筛选条件或输入星图 ID;
|
||||||
|
7. 使用者触发导出或提交批次;
|
||||||
|
8. 插件收集达人数据,按规则过滤、去重、补充字段;
|
||||||
|
9. 插件调用星图接口和公司后端接口补充数据;
|
||||||
|
10. 插件下载 CSV,或把批次提交给后端;
|
||||||
|
11. 使用者通过状态提示、下载文件或后端批次结果确认任务完成。
|
||||||
|
|
||||||
|
### 2.1 安装或更新插件
|
||||||
|
|
||||||
|
- 触发者:使用者或管理员。
|
||||||
|
- 输入:内部 ZIP 安装包,或插件弹窗中发现的新版本安装包。
|
||||||
|
- 处理:解压 ZIP,在 Chrome 扩展页加载 `dist` 文件夹;更新时下载新版 ZIP 后人工重新加载插件。
|
||||||
|
- 输出:Chrome 中安装好的 `Star Chart Search Enhancer` 插件。
|
||||||
|
- 下一步:登录插件。
|
||||||
|
- 人工操作:需要人工解压、加载或重新加载插件。
|
||||||
|
- 条件分支:如果旧版本无法检查更新,需要做一次手动过桥升级。
|
||||||
|
|
||||||
|
### 2.2 登录插件
|
||||||
|
|
||||||
|
- 触发者:使用者点击插件弹窗中的登录按钮。
|
||||||
|
- 输入:公司账号登录态。
|
||||||
|
- 处理:通过 Logto 完成 Chrome 扩展登录,并获取访问后端资源的 token。
|
||||||
|
- 输出:插件弹窗显示已登录状态,内容脚本后续可以挂载业务工具栏。
|
||||||
|
- 下一步:打开星图达人市场页面。
|
||||||
|
- 人工操作:需要使用者完成登录。
|
||||||
|
- 条件分支:未登录或登录过期时,星图页面不会进入导出/提交流程,只显示登录提示。
|
||||||
|
|
||||||
|
### 2.3 打开星图达人市场页面
|
||||||
|
|
||||||
|
- 触发者:使用者访问星图市场页面。
|
||||||
|
- 输入:星图网页登录态、当前页面筛选条件、星图市场列表。
|
||||||
|
- 处理:插件仅在 `xingtu.cn` 域名下的达人市场页面生效;进入页面后先安装页面桥接逻辑,再检查插件登录状态。
|
||||||
|
- 输出:页面上出现插件工具栏和增强列。
|
||||||
|
- 下一步:读取达人列表并补充数据。
|
||||||
|
- 人工操作:使用者需要在星图网页中完成筛选、搜索、翻页或勾选。
|
||||||
|
- 条件分支:如果页面不是星图达人市场页面,插件不启动主流程。
|
||||||
|
|
||||||
|
### 2.4 页面增强和数据补充
|
||||||
|
|
||||||
|
- 触发者:星图页面加载、翻页、列表变化或插件同步周期。
|
||||||
|
- 输入:页面可见达人行、星图列表接口返回值、当前登录用户 token。
|
||||||
|
- 处理:插件读取达人 ID、名称、地区、报价等基础数据,补充看后搜率列和秒思指标列。
|
||||||
|
- 输出:页面上显示加载中、成功、失败或暂无数据等状态。
|
||||||
|
- 下一步:使用者选择导出、按 ID 导出或提交批次。
|
||||||
|
- 人工操作:无。
|
||||||
|
- 条件分支:如果某个指标加载失败,只影响该达人对应指标,不影响页面整体使用。
|
||||||
|
|
||||||
|
### 2.5 导出选中达人数据
|
||||||
|
|
||||||
|
- 触发者:使用者点击 `导出选中达人数据`。
|
||||||
|
- 输入:当前勾选达人、当前导出范围、字段选择配置、传播指标筛选条件。
|
||||||
|
- 处理:必须先勾选达人;插件收集导出范围内的达人,只保留该范围内已勾选的达人,然后补充内容数据、效果预估、画像、秒思指标等字段。
|
||||||
|
- 输出:CSV 文件下载到浏览器默认下载目录。
|
||||||
|
- 下一步:使用者检查 CSV 内容。
|
||||||
|
- 人工操作:需要使用者勾选达人并点击按钮。
|
||||||
|
- 条件分支:如果当前导出范围内没有选中的达人,则不下载 CSV 并提示。
|
||||||
|
|
||||||
|
### 2.6 按星图 ID 导出
|
||||||
|
|
||||||
|
- 触发者:使用者点击 `按星图ID导出`。
|
||||||
|
- 输入:弹窗中粘贴的达人星图 ID。
|
||||||
|
- 处理:插件校验 ID 格式、去重、忽略非法 token,然后逐个 ID 请求基础信息、看后搜率、传播指标、画像、商业能力和后端秒思指标。
|
||||||
|
- 输出:CSV 文件下载到浏览器默认下载目录。
|
||||||
|
- 下一步:使用者检查 CSV 中每个 ID 的导出状态和失败原因。
|
||||||
|
- 人工操作:需要使用者粘贴 ID 并确认。
|
||||||
|
- 条件分支:如果没有有效 ID,不执行导出并提示。
|
||||||
|
|
||||||
|
### 2.7 提交批次
|
||||||
|
|
||||||
|
- 触发者:使用者点击 `提交批次`。
|
||||||
|
- 输入:当前范围或已勾选达人、批次名称、登录用户信息。
|
||||||
|
- 处理:插件先要求输入批次名称,再收集达人数据,应用传播指标阈值筛选和选中规则,检查登录状态,组装批次 payload,提交到后端。
|
||||||
|
- 输出:后端生成批次;页面显示 `批次提交成功` 或失败原因。
|
||||||
|
- 下一步:在后端系统中继续处理批次。
|
||||||
|
- 人工操作:需要使用者输入批次名称。
|
||||||
|
- 条件分支:未登录、批次名为空、后端拒绝或接口失败都会导致本次提交失败。
|
||||||
|
|
||||||
|
## 3. 详细流程说明
|
||||||
|
|
||||||
|
### 3.1 插件安装与更新
|
||||||
|
|
||||||
|
- 步骤目的:让使用者在 Chrome 中获得可运行的内部插件。
|
||||||
|
- 输入内容:内部发布 ZIP、安装说明 PDF、Chrome 浏览器。
|
||||||
|
- 处理规则:
|
||||||
|
- 首次安装需要解压 ZIP;
|
||||||
|
- Chrome 加载的是解压后的 `dist` 文件夹;
|
||||||
|
- 更新时仍然需要人工下载、解压并重新加载;
|
||||||
|
- 扩展 ID 应为 `pkjopdibdnomhogjheclhnknmejccffg`。
|
||||||
|
- 输出结果:Chrome 扩展列表中出现正确插件。
|
||||||
|
- 外部依赖:Chrome 扩展能力;COS 更新文件。
|
||||||
|
- 失败后如何处理:安装失败不会影响外部数据;使用者无法进入后续流程。
|
||||||
|
- 是否影响后续步骤:影响。未安装或安装版本不正确时,后续导出和提交不可用。
|
||||||
|
|
||||||
|
### 3.2 插件登录
|
||||||
|
|
||||||
|
- 步骤目的:获得访问公司后端和受保护接口所需的用户身份。
|
||||||
|
- 输入内容:公司登录账号、Logto 登录配置。
|
||||||
|
- 处理规则:
|
||||||
|
- 登录通过 Chrome identity 回调完成;
|
||||||
|
- 插件读取用户 `sub`、用户名、资源地址和 scope;
|
||||||
|
- 内容脚本进入星图页面时会先读取登录状态。
|
||||||
|
- 输出结果:已登录状态、可用 access token。
|
||||||
|
- 外部依赖:Logto 登录系统。
|
||||||
|
- 失败后如何处理:星图页面显示登录提示;不挂载业务工具栏。
|
||||||
|
- 是否影响后续步骤:影响。批次提交和后端指标查询都依赖 token。
|
||||||
|
|
||||||
|
### 3.3 星图市场页面启动
|
||||||
|
|
||||||
|
- 步骤目的:只在正确页面启用插件能力。
|
||||||
|
- 输入内容:当前浏览器 URL、页面 DOM、星图页面列表请求。
|
||||||
|
- 处理规则:
|
||||||
|
- 只匹配巨量星图达人市场页面;
|
||||||
|
- 进入页面后先安装桥接逻辑,用于捕获星图市场列表请求和页面列表数据;
|
||||||
|
- 未登录时不进入业务控制流程;
|
||||||
|
- 已登录时挂载工具栏和新增列。
|
||||||
|
- 输出结果:插件工具栏、选择框、增强数据列。
|
||||||
|
- 外部依赖:星图网页结构和浏览器内容脚本能力。
|
||||||
|
- 失败后如何处理:如果页面结构变化导致无法挂载,相关功能不可用;未确认是否有统一错误上报。
|
||||||
|
- 是否影响后续步骤:影响。工具栏未挂载时无法导出或提交。
|
||||||
|
|
||||||
|
### 3.4 读取达人列表
|
||||||
|
|
||||||
|
- 步骤目的:确定当前页面或导出范围内有哪些达人。
|
||||||
|
- 输入内容:星图页面列表行、星图列表接口返回值、当前分页状态。
|
||||||
|
- 处理规则:
|
||||||
|
- 优先从星图市场列表接口返回中读取达人;
|
||||||
|
- 关键字段包括达人 ID、达人名称、星图内部传播接口 ID、核心用户 ID、地区、报价和页面可导出字段;
|
||||||
|
- 如果没有捕获到接口请求,则从页面 DOM 读取可见行;
|
||||||
|
- 页面翻页导出时等待页面稳定后再读取;
|
||||||
|
- 同一个达人重复出现时按达人 ID 合并。
|
||||||
|
- 输出结果:标准化后的达人记录集合。
|
||||||
|
- 外部依赖:星图市场列表接口和页面 DOM。
|
||||||
|
- 失败后如何处理:
|
||||||
|
- 单页加载超时会终止本次范围导出;
|
||||||
|
- 单条达人缺少 ID 或名称会被跳过;
|
||||||
|
- 捕获接口失败时退回页面翻页读取。
|
||||||
|
- 是否影响后续步骤:影响。没有达人记录时,导出或提交结果为空或失败。
|
||||||
|
|
||||||
|
### 3.5 页面指标补充
|
||||||
|
|
||||||
|
- 步骤目的:在列表页直接显示补充指标,方便筛选和排序。
|
||||||
|
- 输入内容:当前页面达人 ID。
|
||||||
|
- 处理规则:
|
||||||
|
- 看后搜率优先使用星图列表中已有值;
|
||||||
|
- 如果列表值不完整,再调用星图看后搜率相关接口;
|
||||||
|
- 秒思指标按当前页达人 ID 批量查询后端;
|
||||||
|
- 已成功或已判定缺失的后端指标在当前页面会话中不重复查询;
|
||||||
|
- 指标列支持页面内排序。
|
||||||
|
- 输出结果:页面增强列显示成功值、加载中、加载失败或暂无数据。
|
||||||
|
- 外部依赖:星图接口、公司后端指标接口。
|
||||||
|
- 失败后如何处理:单个达人指标失败只显示失败,不阻塞其他达人。
|
||||||
|
- 是否影响后续步骤:部分影响。导出时会复用已缓存指标,缺失时可能再次补充。
|
||||||
|
|
||||||
|
### 3.6 导出选中达人数据
|
||||||
|
|
||||||
|
- 步骤目的:把使用者选定的达人数据导出为 CSV。
|
||||||
|
- 输入内容:已勾选达人、当前导出范围、字段选择配置、传播指标筛选条件。
|
||||||
|
- 处理规则:
|
||||||
|
- 必须先勾选达人;
|
||||||
|
- 先按导出范围收集达人;
|
||||||
|
- 再严格保留当前导出范围内已勾选的达人;
|
||||||
|
- 对每个达人补充画像、商业能力、传播指标、看后搜率、秒思指标;
|
||||||
|
- 字段选择只控制可选字段,基础字段、导出状态和失败原因等固定保留;
|
||||||
|
- 如果全部画像请求失败,则不下载 CSV 并提示画像导出失败;
|
||||||
|
- 单个达人部分接口失败时,CSV 保留该行,并写入导出状态和失败原因。
|
||||||
|
- 输出结果:CSV 文件。
|
||||||
|
- 外部依赖:星图画像接口、商业能力接口、传播指标接口、公司后端指标接口、浏览器下载能力。
|
||||||
|
- 失败后如何处理:
|
||||||
|
- 没有勾选达人:提示并停止;
|
||||||
|
- 当前范围内无选中达人:提示并停止;
|
||||||
|
- 单个达人部分失败:记录为部分成功或失败;
|
||||||
|
- 全部画像失败:不下载 CSV。
|
||||||
|
- 是否影响后续步骤:不影响外部系统写入;只影响本次下载结果。
|
||||||
|
|
||||||
|
### 3.7 按星图 ID 导出
|
||||||
|
|
||||||
|
- 步骤目的:在不依赖当前星图列表勾选的情况下,批量导出指定 ID 的达人数据。
|
||||||
|
- 输入内容:使用者粘贴的星图 ID 文本。
|
||||||
|
- 处理规则:
|
||||||
|
- 支持用空格、换行、英文逗号、中文逗号、英文分号、中文分号分隔;
|
||||||
|
- 只接受 16 到 20 位纯数字;
|
||||||
|
- 重复 ID 会去重;
|
||||||
|
- 非法 token 会计入提示,但不会进入导出;
|
||||||
|
- 对有效 ID 逐个补齐基础信息、看后搜率、传播指标、画像、商业能力和后端秒思指标;
|
||||||
|
- 每个 ID 生成一行 CSV,并标记成功、部分成功或失败。
|
||||||
|
- 输出结果:按 ID 导出的 CSV 文件。
|
||||||
|
- 外部依赖:星图基础信息接口、星图指标接口、公司后端指标接口、浏览器下载能力。
|
||||||
|
- 失败后如何处理:
|
||||||
|
- 没有有效 ID:提示并停止;
|
||||||
|
- 单个 ID 的部分接口失败:保留该行并写失败原因;
|
||||||
|
- 整体流程异常:提示按 ID 导出失败。
|
||||||
|
- 是否影响后续步骤:不写入外部系统,不影响批次。
|
||||||
|
|
||||||
|
### 3.8 传播指标阈值筛选
|
||||||
|
|
||||||
|
- 步骤目的:在导出或提交前按内容传播表现过滤达人。
|
||||||
|
- 输入内容:视频类别、是否只看指派、是否排除营销流量、时间范围、七个指标阈值。
|
||||||
|
- 处理规则:
|
||||||
|
- 没有填写任何阈值时,不启用该筛选;
|
||||||
|
- 填写多个阈值时必须全部满足;
|
||||||
|
- 个人视频固定为不限指派、不排除营销流量;
|
||||||
|
- 星图视频可选择只看指派、不限指派、排除营销流量或不排除营销流量;
|
||||||
|
- 完播率和互动率按显示百分数比较,例如 `30` 表示 `30%`;
|
||||||
|
- 平均时长按秒比较;
|
||||||
|
- 播放量、评论、点赞、转发按普通数字比较;
|
||||||
|
- 请求失败或缺少被启用指标的达人视为不满足筛选。
|
||||||
|
- 输出结果:过滤后的达人集合。
|
||||||
|
- 外部依赖:星图传播指标接口。
|
||||||
|
- 失败后如何处理:
|
||||||
|
- 阈值非法:阻止导出或提交并提示;
|
||||||
|
- 单个达人筛选请求失败:跳过该达人;
|
||||||
|
- 全部不满足:导出时可能生成只有表头的 CSV;提交批次时会按空记录继续组装并提交,后端是否接受未确认。
|
||||||
|
- 是否影响后续步骤:影响。过滤后的结果才进入 CSV 或批次 payload。
|
||||||
|
|
||||||
|
### 3.9 批次提交
|
||||||
|
|
||||||
|
- 步骤目的:把达人集合提交给后续业务系统。
|
||||||
|
- 输入内容:导出范围内达人、已选达人、传播指标筛选结果、批次名称、登录用户信息。
|
||||||
|
- 处理规则:
|
||||||
|
- 点击后先输入批次名称;
|
||||||
|
- 取消输入则停止;
|
||||||
|
- 批次名为空则提示并停止;
|
||||||
|
- 如果存在已勾选达人,则优先提交当前范围内已勾选达人;
|
||||||
|
- 如果没有勾选达人,则提交当前范围内所有达人;
|
||||||
|
- 如果已勾选达人不在当前范围内,则回退为提交当前范围内所有达人;
|
||||||
|
- 前端不生成批次 ID,批次 ID 由后端生成;
|
||||||
|
- payload 包含登录用户 ID、创建人名称、资源地址、批次名称、创建时间和达人列表;
|
||||||
|
- 达人列表包含达人 ID、达人名称,存在核心用户 ID 时额外带上。
|
||||||
|
- 输出结果:后端批次记录;页面提示提交成功或失败。
|
||||||
|
- 外部依赖:Logto token、批次提交后端。
|
||||||
|
- 失败后如何处理:
|
||||||
|
- 未登录:提示请先登录插件;
|
||||||
|
- token 不可用:提交失败;
|
||||||
|
- 401 或 403:提交失败并返回未授权错误;
|
||||||
|
- 后端返回非成功:提交失败并显示错误;
|
||||||
|
- 网络失败:提交失败。
|
||||||
|
- 是否影响后续步骤:影响外部后端数据。是否会重复创建批次取决于后端,当前项目前端没有确认幂等机制。
|
||||||
|
|
||||||
|
### 3.10 CSV 下载
|
||||||
|
|
||||||
|
- 步骤目的:把导出结果交付给使用者。
|
||||||
|
- 输入内容:生成好的 CSV 字符串和文件名。
|
||||||
|
- 处理规则:
|
||||||
|
- 优先通过 Chrome 扩展后台下载;
|
||||||
|
- 如果扩展下载通道不可用,则使用页面中的临时下载链接;
|
||||||
|
- CSV 带 UTF-8 BOM,方便表格软件识别中文;
|
||||||
|
- 普通导出文件名使用插件名加时间戳;
|
||||||
|
- 按 ID 导出文件名包含按 ID 导出标识。
|
||||||
|
- 输出结果:浏览器下载列表中出现 CSV 文件。
|
||||||
|
- 外部依赖:Chrome downloads 能力或浏览器下载能力。
|
||||||
|
- 失败后如何处理:下载失败会提示;已生成数据不会写入外部系统。
|
||||||
|
- 是否影响后续步骤:不影响外部系统。
|
||||||
|
|
||||||
|
## 4. 接口和外部服务说明
|
||||||
|
|
||||||
|
| 接口/服务 | 用途 | 使用环节 | 核心入参 | 核心返回 | 分页 | 限流 | 并发控制 | 超时 | 重试机制 | 重试次数 | 会重试的情况 | 不会重试的情况 | 凭证/权限 | 额度/成本 | 失败处理 |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 巨量星图网页 | 提供达人市场页面和当前筛选结果 | 页面启动、读取达人列表、人工筛选 | 星图网页登录态、页面筛选条件 | 达人列表页面 | 通过页面分页 | 未确认 | 页面翻页串行执行 | 页面翻页等待约 3 秒,页面稳定等待约 12 秒 | 无自动重试 | 0 | 无 | 页面结构异常、未登录、加载失败 | 星图账号和 cookie | 未确认 | 页面不匹配或结构异常时插件功能不可用 |
|
||||||
|
| `search_for_author_square` | 获取星图达人市场列表数据 | 后台分页导出、读取达人基础字段 | 当前星图列表请求参数、页码 | 达人列表、分页信息、基础字段 | 是 | 未确认 | 后台分页串行请求;`全部` 最多尝试 200 页 | 未单独设置 fetch 超时 | 无自动重试 | 0 | 无 | 请求失败、响应结构异常、解析失败 | 星图网页登录态和 cookie | 未确认 | 请求失败或解析失败时退回页面翻页读取 |
|
||||||
|
| `get_author_commerce_seed_base_info` | 优先获取看后搜率 | 页面增强、导出补充 | `o_author_id`、`range=90` | 商单视频/个人视频看后搜率相关字段 | 否 | 未确认 | 页面增强按当前页达人并发请求;导出补充按达人串行处理 | 8 秒 | 有备用接口回退,但不是同接口重试 | 0 | 非超时失败且未成功时转备用接口 | 超时、备用接口也失败 | 星图网页登录态和 cookie | 未确认 | 请求失败时转备用接口;超时直接记为失败 |
|
||||||
|
| `get_author_ase_info` | 备用获取看后搜率 | 页面增强、导出补充 | `author_id`、`range=30` | 看后搜率相关字段 | 否 | 未确认 | 页面增强按当前页达人并发请求;导出补充按达人串行处理 | 8 秒 | 无自动重试 | 0 | 无 | 任意失败、超时、缺少指标 | 星图网页登录态和 cookie | 未确认 | 失败后标记该达人看后搜率失败 |
|
||||||
|
| `author_audience_distribution` | 获取观众画像 | 导出选中达人、按 ID 导出 | `o_author_id`、`platform_source=1`、`platform_channel=1`、`link_type=5` | 性别、年龄、省份、城市、兴趣、人群等分布 | 否 | 未确认 | 单个达人内画像和商业能力并发;达人之间串行处理 | 8 秒 | 无自动重试 | 0 | 无 | 任意失败、超时、响应缺字段 | 星图网页登录态和 cookie | 未确认 | 单项失败写入失败原因;全部画像失败时不下载选中导出 CSV |
|
||||||
|
| `get_author_fans_distribution` | 获取粉丝画像和铁粉画像 | 导出选中达人、按 ID 导出 | `o_author_id`、`platform_source=1`、`author_type=1 或 5` | 粉丝或铁粉分布 | 否 | 未确认 | 单个达人内多个画像请求串行执行;达人之间串行处理 | 8 秒 | 无自动重试 | 0 | 无 | 任意失败、超时、响应缺字段 | 星图网页登录态和 cookie | 未确认 | 单项失败写入失败原因 |
|
||||||
|
| `get_author_base_info` | 按 ID 导出时获取达人基础信息 | 按星图 ID 导出 | `o_author_id`、`platform_source=1`、`platform_channel=1`、`recommend=true` 等 | 达人名称等基础信息 | 否 | 未确认 | 按 ID 导出中与看后搜率并发;不同 ID 串行处理 | 8 秒 | 无自动重试 | 0 | 无 | 任意失败、超时、响应缺字段 | 星图网页登录态和 cookie | 未确认 | 单个 ID 基础信息失败,CSV 中记录失败 |
|
||||||
|
| `get_author_commerce_spread_info` | 获取商业能力和效果预估 | 导出选中达人、按 ID 导出 | `o_author_id` | 预期 CPM、预期 CPE、预期播放量、爆文率 | 否 | 未确认 | 与画像请求并发;达人之间串行处理 | 8 秒 | 无自动重试 | 0 | 无 | 任意失败或超时 | 星图网页登录态和 cookie | 未确认 | 单项失败写入失败原因,其他数据继续 |
|
||||||
|
| `get_author_spread_info` | 获取内容传播指标,并用于阈值筛选 | 内容数据导出、阈值筛选 | `o_author_id`、`platform_source=1`、`platform_channel=1`、`type`、`flow_type`、`only_assign`、`range` | 完播率、播放量中位数、互动率、平均时长、平均评论、平均点赞、平均转发 | 否 | 未确认 | 指标补充对达人并发;单个达人内多组参数串行;筛选对达人并发 | 8 秒 | 无自动重试 | 0 | 无 | 任意失败、超时、响应缺字段 | 星图网页登录态和 cookie | 未确认 | 指标补充失败时字段留空;筛选请求失败时该达人不满足筛选 |
|
||||||
|
| talent-search 后端 `POST /api/v1/history/talents/search` | 查询秒思 api 指标 | 页面增强、CSV 导出、按 ID 导出 | Bearer token、`type=star_id`、`values`、`page=1`、`size=max(20, ID数量)` | 看后搜率、看后搜数、新增 A3、CPA3、cp_search 等 | 请求固定第一页;接口本身是否支持更多页未确认 | 未确认 | 按页面或导出集合批量请求;同一批只有一个请求 | 未确认 | 无自动重试 | 0 | 无 | token 失败、请求失败、响应结构异常 | Logto access token,当前 resource 为 talent-search | 未确认 | 页面增强中失败标记后端指标失败;导出中失败则相关字段为空 |
|
||||||
|
| 批次提交后端 `POST /api/v1/batch-status/batches` | 创建达人批次 | 提交批次 | Bearer token、批次名称、创建人、达人列表 | 成功标志和后端数据 | 否 | 未确认 | 每次点击提交只发一个请求;按钮忙碌态防止流程内重复点击 | 未确认 | 无自动重试 | 0 | 无 | 401、403、非 2xx、后端 `success` 非 true、网络失败 | Logto access token;写权限 scope 是否足够未确认 | 未确认 | 401/403 或非成功响应会终止本次提交 |
|
||||||
|
| Logto | 插件登录和获取访问 token | 登录、后端接口调用 | appId、resource、scope、Chrome redirect URL | 登录态、ID claims、access token | 否 | 未确认 | 由 Logto SDK 管理,项目内未设并发规则 | 未确认 | 项目内无自动重试;SDK 内部是否重试未确认 | 未确认 | 未确认 | 登录失败、token 不可用、授权不足 | 公司 Logto 账号和 Chrome identity 回调权限 | 未确认 | 登录失败或 token 不可用时不进入业务流程,或后端调用失败 |
|
||||||
|
| COS 更新清单 | 检查插件新版本 | 插件弹窗 | `latest.json` URL | 最新版本、ZIP URL、说明 PDF URL、发布时间、更新说明 | 否 | 未确认 | 弹窗打开后单次检查 | 未确认 | 无自动重试 | 0 | 无 | 请求失败、清单格式错误、URL 非 HTTPS | 清单和安装包需公开可读 | COS 存储和流量成本未确认 | 检查失败时弹窗显示无法检查更新,不影响已安装插件主功能 |
|
||||||
|
| Chrome downloads | 下载 CSV、更新包和说明 PDF | CSV 导出、插件更新 | 文件名、下载 URL 或 data URL | 浏览器下载任务 | 否 | 浏览器自身规则,未确认 | 由浏览器管理 | 未确认 | 无自动重试 | 0 | 无 | 下载权限不可用、浏览器拦截、URL 无效 | Chrome `downloads` 权限 | 未确认 | CSV 下载失败时回退页面链接下载;更新包下载失败时显示错误 |
|
||||||
|
|
||||||
|
凭证和权限说明:
|
||||||
|
|
||||||
|
- 星图接口依赖当前浏览器中的星图网页登录态和 cookie;
|
||||||
|
- 公司后端指标查询和批次提交依赖 Logto access token;
|
||||||
|
- 插件需要 Chrome `downloads`、`identity`、`storage` 权限;
|
||||||
|
- COS 更新包需要公开可读;
|
||||||
|
- 具体接口额度、调用成本、账号级限制均未确认。
|
||||||
|
|
||||||
|
## 5. 数据处理规则
|
||||||
|
|
||||||
|
### 5.1 数据来源
|
||||||
|
|
||||||
|
数据主要来自四类来源:
|
||||||
|
|
||||||
|
- 星图市场页面和列表接口:达人 ID、名称、地区、报价、粉丝、内容主题、预期播放、互动率、完播率等基础字段;
|
||||||
|
- 星图详情类接口:看后搜率、画像、商业能力、传播指标;
|
||||||
|
- 公司 talent-search 后端:秒思 api 指标;
|
||||||
|
- 使用者输入:勾选状态、星图 ID、批次名称、字段选择和阈值筛选条件。
|
||||||
|
|
||||||
|
### 5.2 保留规则
|
||||||
|
|
||||||
|
- 有有效达人 ID 和达人名称的记录会进入页面记录集合;
|
||||||
|
- 按 ID 导出时,有效 ID 即使部分接口失败也会生成 CSV 行;
|
||||||
|
- CSV 基础字段、导出状态、失败原因等固定字段会保留;
|
||||||
|
- 字段选择只影响可选数据字段,不删除固定标识字段。
|
||||||
|
|
||||||
|
### 5.3 过滤规则
|
||||||
|
|
||||||
|
- 星图页面中缺少达人 ID 或达人名称的行会跳过;
|
||||||
|
- 导出选中达人数据必须有已勾选达人;
|
||||||
|
- 画像导出只保留当前导出范围内的已勾选达人;
|
||||||
|
- 普通导出或提交批次如果存在已选达人,会优先保留当前范围内的已选达人;
|
||||||
|
- 如果当前范围内没有任何已选达人,普通导出或提交批次会回退为当前范围全部达人;
|
||||||
|
- 传播指标阈值筛选启用后,不满足全部阈值的达人会被过滤;
|
||||||
|
- 按 ID 导出时,非 16 到 20 位纯数字 token 会过滤。
|
||||||
|
|
||||||
|
### 5.4 去重规则
|
||||||
|
|
||||||
|
- 多页导出和后台分页导出按达人 ID 合并去重;
|
||||||
|
- 按 ID 导出对输入 ID 去重;
|
||||||
|
- 合并时优先保留已有的非空字段;
|
||||||
|
- 指标字段会在有新非空值时补充。
|
||||||
|
|
||||||
|
### 5.5 字段补充和合并规则
|
||||||
|
|
||||||
|
- 星图列表数据作为基础;
|
||||||
|
- 看后搜率优先使用列表中已有值,不完整时再请求星图指标接口;
|
||||||
|
- 秒思指标按 star_id 从后端补充;
|
||||||
|
- 传播指标按多组参数生成不同列,不合并同名业务指标;
|
||||||
|
- `代表视频`可能被读取,但不会进入最终普通市场 CSV;
|
||||||
|
- 画像和商业能力字段追加在基础字段之后;
|
||||||
|
- 传播指标字段追加在基础字段、看后搜率和秒思 api 字段之后。
|
||||||
|
|
||||||
|
### 5.6 覆盖规则
|
||||||
|
|
||||||
|
- 当前页面会话内的内存记录会被补充和合并;
|
||||||
|
- 项目不会把 CSV 导出结果写回星图;
|
||||||
|
- 批次提交会向后端创建或提交数据,是否覆盖已有批次未确认;
|
||||||
|
- 字段选择配置会保存到浏览器 localStorage,下次导出沿用。
|
||||||
|
|
||||||
|
### 5.7 写入位置
|
||||||
|
|
||||||
|
- CSV 写入浏览器下载目录;
|
||||||
|
- 批次数据写入批次提交后端;
|
||||||
|
- 字段选择写入浏览器 localStorage;
|
||||||
|
- 登录状态由 Logto Chrome 扩展 SDK 管理;
|
||||||
|
- 项目本身不维护持久任务数据库。
|
||||||
|
|
||||||
|
### 5.8 写入失败处理
|
||||||
|
|
||||||
|
- CSV 下载失败会提示或使用备用下载方式;
|
||||||
|
- 批次提交失败会提示失败原因,本次提交不视为成功;
|
||||||
|
- 字段选择保存失败会被忽略,后续可能恢复默认全选字段;
|
||||||
|
- 后端指标补充失败不会阻止 CSV 生成,只会导致字段为空。
|
||||||
|
|
||||||
|
### 5.9 重复运行时数据变化
|
||||||
|
|
||||||
|
- 重复导出会重新生成新的 CSV 文件;
|
||||||
|
- 重复按 ID 导出不会写入外部业务系统;
|
||||||
|
- 重复提交批次可能在后端产生重复批次,是否由后端去重未确认;
|
||||||
|
- 页面内已缓存的成功指标可能在当前会话中复用,刷新页面后会重新读取。
|
||||||
|
|
||||||
|
## 6. 重复执行、中断恢复和幂等性
|
||||||
|
|
||||||
|
- 任务可以重复执行。
|
||||||
|
- CSV 导出重复执行会产生新的下载文件,不会覆盖星图或后端数据。
|
||||||
|
- 按 ID 导出重复执行会重新请求接口并生成新 CSV,不会写入后端批次。
|
||||||
|
- 字段选择重复保存会覆盖浏览器本地保存的字段选择。
|
||||||
|
- 批次提交重复执行是否会重复创建批次:未确认。当前前端没有批次幂等键,也不生成 batchId。
|
||||||
|
- 批次提交是否覆盖已有结果:未确认,取决于后端。
|
||||||
|
- 任务跑到一半失败后,当前项目没有持久任务状态记录。
|
||||||
|
- 导出中断后再次执行会从本次流程开头重新收集和请求,不会从上次中断点恢复。
|
||||||
|
- 页面会话中的指标缓存可以减少同一页面内重复请求,但不等同于断点续跑。
|
||||||
|
- 浏览器刷新、插件重载或页面关闭会丢失内存中的中间状态。
|
||||||
|
- 多页导出按达人 ID 去重,重复分页读取同一达人不会在 CSV 中重复出现。
|
||||||
|
- 按 ID 导出对输入 ID 去重,重复输入同一 ID 不会产生重复行。
|
||||||
|
- 阈值筛选没有持久状态,重新执行时按当前页面输入框值重新判断。
|
||||||
|
|
||||||
|
重复执行相对安全的操作:
|
||||||
|
|
||||||
|
- 重新打开页面;
|
||||||
|
- 重新导出 CSV;
|
||||||
|
- 重新按 ID 导出;
|
||||||
|
- 重新检查更新;
|
||||||
|
- 重新保存字段选择。
|
||||||
|
|
||||||
|
重复执行可能有风险的操作:
|
||||||
|
|
||||||
|
- 重复点击提交批次;
|
||||||
|
- 修改后端地址后提交批次;
|
||||||
|
- 使用不同星图筛选条件或不同字段选择重复导出后,拿多个 CSV 混用;
|
||||||
|
- 阈值输入为空或变化后重复提交,可能导致提交达人集合变化。
|
||||||
|
|
||||||
|
未确认项:
|
||||||
|
|
||||||
|
- 后端批次接口是否按批次名称、用户或达人集合去重;
|
||||||
|
- 后端批次接口是否允许空达人列表;
|
||||||
|
- 后端是否有任务状态、失败重试或重复提交保护;
|
||||||
|
- Logto token 刷新失败后是否有 SDK 内部重试。
|
||||||
|
|
||||||
|
## 7. 项目使用方式
|
||||||
|
|
||||||
|
### 7.1 使用前准备
|
||||||
|
|
||||||
|
使用前需要准备:
|
||||||
|
|
||||||
|
- Google Chrome 浏览器;
|
||||||
|
- 内部发布的 `star-chart-search-enhancer-internal.zip`;
|
||||||
|
- 可访问巨量星图的账号;
|
||||||
|
- 可访问公司 Logto 登录系统的账号;
|
||||||
|
- 如果要提交批次,需要批次提交后端可访问;
|
||||||
|
- 如果要查询秒思 api 指标,需要 talent-search 后端授权可用。
|
||||||
|
|
||||||
|
需要的权限和凭证:
|
||||||
|
|
||||||
|
- Chrome 扩展加载权限;
|
||||||
|
- 星图网页登录态;
|
||||||
|
- Logto 登录态;
|
||||||
|
- talent-search 后端读取权限;
|
||||||
|
- 批次提交后端所需权限,具体 scope 是否只需当前配置未确认。
|
||||||
|
|
||||||
|
### 7.2 本地安装使用
|
||||||
|
|
||||||
|
1. 解压内部 ZIP;
|
||||||
|
2. 打开 `chrome://extensions`;
|
||||||
|
3. 开启开发者模式;
|
||||||
|
4. 点击加载已解压的扩展程序;
|
||||||
|
5. 选择解压后的 `dist` 文件夹;
|
||||||
|
6. 确认扩展 ID 为 `pkjopdibdnomhogjheclhnknmejccffg`;
|
||||||
|
7. 固定插件图标;
|
||||||
|
8. 点击插件图标并登录;
|
||||||
|
9. 打开 `https://xingtu.cn/ad/creator/market`;
|
||||||
|
10. 等待插件工具栏出现。
|
||||||
|
|
||||||
|
### 7.3 执行一次完整导出任务
|
||||||
|
|
||||||
|
1. 在星图市场中完成筛选;
|
||||||
|
2. 等待页面列表加载完成;
|
||||||
|
3. 勾选需要导出的达人;
|
||||||
|
4. 可选:点击 `选择字段` 调整 CSV 字段;
|
||||||
|
5. 可选:填写传播指标阈值;
|
||||||
|
6. 点击 `导出选中达人数据`;
|
||||||
|
7. 等待状态提示从导出中消失或浏览器下载完成;
|
||||||
|
8. 在下载目录或 Chrome 下载列表中查看 CSV;
|
||||||
|
9. 检查 `导出状态` 和 `失败原因` 字段。
|
||||||
|
|
||||||
|
### 7.4 按 ID 执行导出任务
|
||||||
|
|
||||||
|
1. 点击 `按星图ID导出`;
|
||||||
|
2. 粘贴达人星图 ID,每行一个或用分隔符隔开;
|
||||||
|
3. 点击确认;
|
||||||
|
4. 查看识别数量、去重后数量和非法数量提示;
|
||||||
|
5. 等待 CSV 下载;
|
||||||
|
6. 检查每行导出状态和失败原因。
|
||||||
|
|
||||||
|
### 7.5 执行一次批次提交
|
||||||
|
|
||||||
|
1. 在星图市场中完成筛选;
|
||||||
|
2. 可选:勾选需要提交的达人;
|
||||||
|
3. 可选:填写传播指标阈值;
|
||||||
|
4. 点击 `提交批次`;
|
||||||
|
5. 输入批次名称;
|
||||||
|
6. 等待页面提示 `批次提交成功`;
|
||||||
|
7. 到后端系统确认批次是否生成。
|
||||||
|
|
||||||
|
### 7.6 只执行某个子流程
|
||||||
|
|
||||||
|
- 只登录:打开插件弹窗并登录;
|
||||||
|
- 只检查更新:登录后打开插件弹窗查看版本更新区域;
|
||||||
|
- 只选择字段:在星图市场页点击 `选择字段` 并保存;
|
||||||
|
- 只按 ID 导出:不需要勾选页面达人,直接点击 `按星图ID导出`;
|
||||||
|
- 只提交批次:不需要先导出 CSV,但需要登录并输入批次名称。
|
||||||
|
|
||||||
|
### 7.7 重新执行任务
|
||||||
|
|
||||||
|
- 重新导出:直接再次点击导出按钮;
|
||||||
|
- 重新按 ID 导出:再次打开 ID 输入弹窗并确认;
|
||||||
|
- 重新提交批次:再次点击提交批次并输入批次名称。注意可能创建重复批次,后端幂等未确认;
|
||||||
|
- 页面异常后重试:刷新星图页面,等待工具栏重新出现后再执行。
|
||||||
|
|
||||||
|
### 7.8 确认任务完成
|
||||||
|
|
||||||
|
- 导出任务:浏览器下载列表出现 CSV 文件;
|
||||||
|
- 按 ID 导出:CSV 文件名包含按 ID 导出标识,且文件中有导出状态列;
|
||||||
|
- 批次提交:页面显示 `批次提交成功`,并在后端系统中能看到对应批次;
|
||||||
|
- 插件更新:重新加载后插件版本显示为新版本。
|
||||||
|
|
||||||
|
### 7.9 危险操作
|
||||||
|
|
||||||
|
- 重复点击 `提交批次`;
|
||||||
|
- 修改后端地址后未验证就发给同事使用;
|
||||||
|
- 删除正在被 Chrome 加载的 `dist` 文件夹;
|
||||||
|
- 随意修改 Logto 配置、后端地址、scope 或 manifest key;
|
||||||
|
- 在未确认星图筛选条件的情况下提交全部范围达人;
|
||||||
|
- 阈值筛选填错导致提交集合被大幅改变。
|
||||||
|
|
||||||
|
### 7.10 不能随便改的参数
|
||||||
|
|
||||||
|
- 固定扩展 ID 相关配置;
|
||||||
|
- Logto appId、endpoint、resource、scope;
|
||||||
|
- 批次提交后端地址;
|
||||||
|
- talent-search 后端地址;
|
||||||
|
- COS 更新清单 URL;
|
||||||
|
- 星图接口参数含义;
|
||||||
|
- 传播指标列名规则;
|
||||||
|
- 批次 payload 字段。
|
||||||
|
|
||||||
|
### 7.11 运行和发布方式
|
||||||
|
|
||||||
|
本地开发运行:
|
||||||
|
|
||||||
|
- 安装依赖:`npm install`;
|
||||||
|
- 运行测试:`npm test`;
|
||||||
|
- 开发构建:`npm run build`;
|
||||||
|
- 然后在 Chrome 中加载 `dist`。
|
||||||
|
|
||||||
|
内部发布构建:
|
||||||
|
|
||||||
|
- 运行测试;
|
||||||
|
- 执行内部打包;
|
||||||
|
- 生成 ZIP 和更新清单;
|
||||||
|
- 上传或分发给同事;
|
||||||
|
- 同事仍需人工解压和加载。
|
||||||
|
|
||||||
|
定时任务运行:
|
||||||
|
|
||||||
|
- 当前项目未确认存在服务端定时任务。
|
||||||
|
- 插件更新发布可通过 tag 触发 Drone 发布流程。
|
||||||
|
|
||||||
|
## 8. 运行参数和配置说明
|
||||||
|
|
||||||
|
| 配置名称 | 作用 | 默认值 | 可选值 | 修改影响 | 是否需要重启/重载 | 风险 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 扩展 ID / manifest key | 固定 Chrome 扩展身份 | `pkjopdibdnomhogjheclhnknmejccffg` | 未确认 | 影响 Logto 回调、用户安装识别、更新连续性 | 需要重新构建并重新加载插件 | 改错会导致登录失败或同事装到不同插件 |
|
||||||
|
| Logto endpoint | 登录服务地址 | `https://login-api.intelligrow.cn` | 未确认 | 影响登录和 token 获取 | 需要重新构建并重新加载插件 | 登录不可用 |
|
||||||
|
| Logto appId | 插件登录应用 ID | `i4jkllbvih0554r4n0fd3` | 未确认 | 影响登录应用和回调校验 | 需要重新构建并重新加载插件 | 登录不可用 |
|
||||||
|
| apiResource | token 资源地址 | `https://talent-search.intelligrow.cn` | 未确认 | 影响后端 token audience/resource | 需要重新构建并重新加载插件 | 后端接口 401/403 |
|
||||||
|
| scopes | 登录申请权限 | `openid`、`profile`、`offline_access`、`talent-search:read` | 未确认 | 影响 token 权限 | 需要重新登录;通常也需重新构建 | 后端读写权限不足 |
|
||||||
|
| enableDevAuthPanel | 是否显示开发调试面板 | `false` | `true` / `false` | 影响插件弹窗是否显示调试入口 | 需要重新构建并重新加载插件 | 暴露调试入口 |
|
||||||
|
| 批次提交后端地址 | 提交达人批次的目标服务 | 当前工作区为 `http://localhost:8083` | 其他后端地址未确认 | 影响批次提交去向 | 需要重新构建并重新加载插件 | 提交到错误环境、重复或丢失业务数据 |
|
||||||
|
| 后端指标服务地址 | 查询秒思 api 指标 | `https://talent-search.intelligrow.cn` | 未确认 | 影响页面增强列和 CSV 秒思字段 | 需要重新构建并重新加载插件 | 指标为空、权限错误或消耗错误环境额度 |
|
||||||
|
| COS 更新清单 URL | 插件弹窗检查新版本 | `https://wksgx-1343191620.cos.ap-nanjing.myqcloud.com/star-chart-search-enhancer/latest.json` | 其他 HTTPS URL | 影响更新提示和安装包下载 | 需要重新构建并重新加载插件 | 用户无法更新或下载错误包 |
|
||||||
|
| 导出范围 | 决定收集哪些页面达人 | 当前工具栏默认隐藏,默认值为前 5 页;当前用户主入口通常要求勾选达人 | 当前页、前 5 页、前 10 页、全部、自定义 | 影响导出或提交的达人集合 | 不需要重启 | 范围过大增加接口调用量 |
|
||||||
|
| 传播指标阈值 | 导出或提交前二次过滤达人 | 空 | 非负数字 | 影响最终保留达人集合 | 不需要重启 | 填错会过滤掉目标达人 |
|
||||||
|
| 字段选择 | 控制 CSV 可选字段 | 默认全选 | 可选字段集合 | 影响 CSV 列 | 不需要重启,会本地保存 | 漏导业务字段 |
|
||||||
|
|
||||||
|
## 9. 任务执行和结果确认
|
||||||
|
|
||||||
|
### 9.1 任务开始标志
|
||||||
|
|
||||||
|
- 插件登录任务:点击插件弹窗登录按钮后跳转登录;
|
||||||
|
- 页面增强任务:星图市场页面出现插件工具栏和新增列;
|
||||||
|
- 导出任务:状态区出现 `画像导出中`、`按ID画像导出中` 或类似导出中提示;
|
||||||
|
- 批次提交任务:点击提交并输入批次名称后,状态区出现提交中提示;
|
||||||
|
- 更新检查任务:插件弹窗显示正在检查更新。
|
||||||
|
|
||||||
|
### 9.2 执行中状态
|
||||||
|
|
||||||
|
- 工具栏按钮会被禁用,防止同一流程中重复点击;
|
||||||
|
- 多页收集时会显示页码进度;
|
||||||
|
- 画像和按 ID 导出会显示当前处理序号;
|
||||||
|
- 页面指标列可能显示 `加载中...`;
|
||||||
|
- 后端指标可能显示暂无数据或加载失败。
|
||||||
|
|
||||||
|
### 9.3 成功完成标志
|
||||||
|
|
||||||
|
- CSV 导出:浏览器下载列表出现 CSV 文件;
|
||||||
|
- 按 ID 导出:下载完成,CSV 中每行有导出状态;
|
||||||
|
- 批次提交:页面提示 `批次提交成功`;
|
||||||
|
- 更新下载:弹窗提示已触发下载;
|
||||||
|
- 页面增强:指标列显示具体数值或明确的暂无数据状态。
|
||||||
|
|
||||||
|
### 9.4 部分成功表现
|
||||||
|
|
||||||
|
- 单个达人部分接口失败时,CSV 中该行 `导出状态` 为部分成功或失败,并在 `失败原因` 中列明失败项;
|
||||||
|
- 秒思 api 指标失败时,对应字段为空或页面显示失败,不一定影响 CSV 下载;
|
||||||
|
- 传播指标某组参数失败时,对应字段为空;
|
||||||
|
- 画像部分失败时,其他画像或商业能力字段仍可保留;
|
||||||
|
- 后端指标查询不到某个达人时显示暂无数据。
|
||||||
|
|
||||||
|
### 9.5 失败表现
|
||||||
|
|
||||||
|
- 未登录:星图页面显示登录提示,不出现业务工具栏;
|
||||||
|
- 没勾选就导出选中达人数据:提示请先勾选;
|
||||||
|
- 当前范围无选中达人:提示当前导出范围内没有选中的达人;
|
||||||
|
- 全部画像失败:提示画像导出失败,不下载 CSV;
|
||||||
|
- 按 ID 没有有效 ID:提示请输入有效的达人星图 ID;
|
||||||
|
- 批次提交失败:状态区显示接口错误或通用失败提示;
|
||||||
|
- 更新清单失败:弹窗显示暂时无法检查更新或错误信息。
|
||||||
|
|
||||||
|
### 9.6 最终结果查看位置
|
||||||
|
|
||||||
|
- CSV:浏览器默认下载目录或 Chrome 下载列表;
|
||||||
|
- 批次:批次提交后端系统,具体查看入口未确认;
|
||||||
|
- 插件版本:插件弹窗或 Chrome 扩展详情页;
|
||||||
|
- 登录状态:插件弹窗;
|
||||||
|
- 页面增强结果:星图达人市场页面新增列。
|
||||||
|
|
||||||
|
### 9.7 管理者确认标准
|
||||||
|
|
||||||
|
管理者确认一次任务是否达到预期时,应关注:
|
||||||
|
|
||||||
|
- 使用者是否登录了正确插件;
|
||||||
|
- 星图筛选条件是否符合业务目标;
|
||||||
|
- 导出的 CSV 行数是否符合已选达人或输入 ID 数量;
|
||||||
|
- CSV 中 `导出状态` 是否大部分为成功;
|
||||||
|
- 关键业务字段是否有值,例如内容数据、效果预估、画像、秒思 api 数据;
|
||||||
|
- 批次提交是否在后端生成对应批次;
|
||||||
|
- 批次名称、创建人和达人数量是否符合预期;
|
||||||
|
- 是否存在重复提交批次。
|
||||||
|
|
||||||
|
## 10. 重要限制和风险
|
||||||
|
|
||||||
|
- 星图接口调用额度限制:未确认。
|
||||||
|
- 星图接口限流规则:未确认。
|
||||||
|
- 公司后端接口额度限制:未确认。
|
||||||
|
- 批次提交接口幂等规则:未确认。
|
||||||
|
- 项目没有持久任务状态记录,不支持真正断点续跑。
|
||||||
|
- 导出范围过大时,会产生大量星图接口请求,运行时间会变长。
|
||||||
|
- 当前传播指标补充和筛选存在并发请求;是否有显式并发上限未确认,当前未看到稳定的业务级并发限制配置。
|
||||||
|
- 星图页面结构变化可能导致工具栏挂载、列表读取或翻页失效。
|
||||||
|
- 星图网页登录态过期会导致接口失败。
|
||||||
|
- Logto token 不可用会导致后端指标和批次提交失败。
|
||||||
|
- 批次提交重复执行可能产生重复批次。
|
||||||
|
- 修改后端地址可能把数据提交到错误环境。
|
||||||
|
- 字段选择保存到本地浏览器,换浏览器或清理数据后会恢复默认。
|
||||||
|
- 更新包仍需人工解压和重载,下载新版本不等于插件已更新。
|
||||||
|
- 删除或移动本地 `dist` 文件夹会导致已加载插件失效。
|
||||||
|
- 扩展 ID、Logto 回调和 manifest key 强相关,改错会导致登录失败。
|
||||||
|
- `http://localhost:8083` 作为批次提交默认地址时,只适合本机后端可用的场景;生产或同事环境是否适用未确认。
|
||||||
|
- 下载 CSV 不会自动校验业务完整性,需要使用者或管理者检查导出状态和关键字段。
|
||||||
|
- 传播指标阈值填错会改变导出或提交达人集合。
|
||||||
|
|
||||||
|
## 11. 未确认项清单
|
||||||
|
|
||||||
|
- 星图各接口是否存在明确限流:未确认。
|
||||||
|
- 星图各接口账号级、IP 级或 cookie 级调用额度:未确认。
|
||||||
|
- 星图各接口失败后是否由浏览器或服务端内部重试:未确认。
|
||||||
|
- 公司后端 `history/talents/search` 是否有分页上限、查询数量上限或限流:未确认。
|
||||||
|
- 公司后端 `history/talents/search` 的接口超时时间:未确认。
|
||||||
|
- 批次提交后端的生产地址:未确认;当前工作区配置为 `http://localhost:8083`。
|
||||||
|
- 批次提交后端是否支持幂等:未确认。
|
||||||
|
- 批次提交后端是否允许空达人列表:未确认。
|
||||||
|
- 批次提交后端是否会覆盖同名批次:未确认。
|
||||||
|
- 批次提交后端生成的 7 位数字批次 ID 的具体规则:未确认。
|
||||||
|
- 批次提交后端的查看入口和管理流程:未确认。
|
||||||
|
- 当前 Logto scope 是否足够覆盖批次写操作:未确认。
|
||||||
|
- COS 更新清单的权限、缓存和发布审核规则:未确认。
|
||||||
|
- Drone 发布是否为唯一正式发布方式:未确认。
|
||||||
|
- 插件是否有统一日志、错误上报或审计记录:未确认。
|
||||||
|
- 页面增强指标是否有跨页面或跨浏览器持久缓存:未确认;当前仅确认有页面会话内记录。
|
||||||
|
- 导出全部页面时最多导出多少页:后台静默导出当前最多尝试 200 页;真实星图侧上限未确认。
|
||||||
|
- 传播指标请求是否应该限制并发:需求文档曾提出需要限制,但当前真实业务级并发控制未确认。
|
||||||
|
- 后续业务系统如何消费批次:未确认。
|
||||||
|
|
||||||
|
## 12. 文档维护规则
|
||||||
|
|
||||||
|
从现在开始,任何模型或工程师在修改本项目代码时,都必须遵守以下规则:
|
||||||
|
|
||||||
|
1. 修改代码前,先检查本流程文档是否描述了相关流程;
|
||||||
|
2. 如果代码改动影响流程、接口、配置、数据处理规则、使用方式、任务执行方式、结果确认方式、限流、重试、超时、并发或幂等性,必须同步更新本文档;
|
||||||
|
3. 如果代码行为和文档描述发生冲突,必须以当前真实行为为准更新文档;
|
||||||
|
4. 如果改动较大,但判断不需要更新文档,必须说明原因;
|
||||||
|
5. 每次完成较大代码改动后,最终回复必须明确说明:
|
||||||
|
- 是否检查了流程文档;
|
||||||
|
- 是否更新了流程文档;
|
||||||
|
- 更新了哪些部分;
|
||||||
|
- 如果没有更新,为什么不需要更新。
|
||||||
|
|
||||||
|
以下情况都视为必须检查文档的较大改动:
|
||||||
|
|
||||||
|
- 改变整体流程顺序;
|
||||||
|
- 新增、删除或调整流程步骤;
|
||||||
|
- 新增、删除或替换外部接口;
|
||||||
|
- 修改接口参数、鉴权方式、分页方式、限流方式、重试方式、超时规则或并发规则;
|
||||||
|
- 修改数据过滤、去重、合并、覆盖、写入规则;
|
||||||
|
- 修改任务启动方式、运行参数或配置项;
|
||||||
|
- 修改任务成功、失败、部分成功的判断方式;
|
||||||
|
- 修改重复执行、中断恢复或幂等性行为;
|
||||||
|
- 修改最终结果的产出位置或格式;
|
||||||
|
- 修改会影响项目使用者操作方式的任何内容。
|
||||||
@ -59,7 +59,7 @@ const hostPermissionsByTarget = {
|
|||||||
"https://*.xingtu.cn/ad/creator/market*",
|
"https://*.xingtu.cn/ad/creator/market*",
|
||||||
"https://login-api.intelligrow.cn/*",
|
"https://login-api.intelligrow.cn/*",
|
||||||
"https://talent-search.intelligrow.cn/*",
|
"https://talent-search.intelligrow.cn/*",
|
||||||
"http://192.168.31.21:8083/*",
|
"http://localhost:8083/*",
|
||||||
"https://*/*"
|
"https://*/*"
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -162,10 +162,10 @@ export function ensurePluginToolbar(
|
|||||||
dataGroup.dataset.pluginToolbarGroup = "data";
|
dataGroup.dataset.pluginToolbarGroup = "data";
|
||||||
applyToolbarGroupStyles(dataGroup);
|
applyToolbarGroupStyles(dataGroup);
|
||||||
dataGroup.append(
|
dataGroup.append(
|
||||||
createToolbarGroupTitle(document, "达人数据"),
|
|
||||||
audienceProfileExportButton,
|
audienceProfileExportButton,
|
||||||
audienceProfileByIdExportButton,
|
audienceProfileByIdExportButton,
|
||||||
audienceProfileFieldButton
|
audienceProfileFieldButton,
|
||||||
|
batchSubmitButton
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoGroup = document.createElement("div");
|
const videoGroup = document.createElement("div");
|
||||||
@ -181,30 +181,23 @@ export function ensurePluginToolbar(
|
|||||||
|
|
||||||
const thresholdGroup = document.createElement("div");
|
const thresholdGroup = document.createElement("div");
|
||||||
thresholdGroup.dataset.pluginToolbarGroup = "thresholds";
|
thresholdGroup.dataset.pluginToolbarGroup = "thresholds";
|
||||||
applyToolbarGroupStyles(thresholdGroup);
|
applyThresholdGroupStyles(thresholdGroup);
|
||||||
thresholdGroup.append(
|
thresholdGroup.append(
|
||||||
createToolbarGroupTitle(document, "传播指标"),
|
...createSpreadThresholdControls(document, spreadThresholdInputs)
|
||||||
...Object.values(spreadThresholdInputs)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const divider = document.createElement("span");
|
const thresholdTitle = createToolbarGroupTitle(document, "传播指标筛选");
|
||||||
applyToolbarDividerStyles(divider);
|
const thresholdRule = createSpreadThresholdRule(document);
|
||||||
|
|
||||||
const actions = document.createElement("div");
|
firstRow.append(dataGroup, videoGroup, exportStatusText);
|
||||||
actions.dataset.pluginToolbarActions = "root";
|
secondRow.append(thresholdTitle, thresholdRule, thresholdGroup);
|
||||||
applyToolbarActionStyles(actions);
|
|
||||||
actions.append(batchSubmitButton);
|
|
||||||
|
|
||||||
firstRow.append(dataGroup, divider, videoGroup, exportStatusText);
|
|
||||||
secondRow.append(thresholdGroup);
|
|
||||||
panel.append(firstRow, secondRow);
|
panel.append(firstRow, secondRow);
|
||||||
|
|
||||||
root.append(
|
root.append(
|
||||||
exportRangeSelect,
|
exportRangeSelect,
|
||||||
exportCustomPagesInput,
|
exportCustomPagesInput,
|
||||||
exportButton,
|
exportButton,
|
||||||
panel,
|
panel
|
||||||
actions
|
|
||||||
);
|
);
|
||||||
|
|
||||||
document.body.appendChild(root);
|
document.body.appendChild(root);
|
||||||
@ -220,7 +213,7 @@ export function ensurePluginToolbar(
|
|||||||
spreadFilterOnlyAssignSelect,
|
spreadFilterOnlyAssignSelect,
|
||||||
spreadFilterRangeSelect,
|
spreadFilterRangeSelect,
|
||||||
spreadFilterTypeSelect,
|
spreadFilterTypeSelect,
|
||||||
...Object.values(spreadThresholdInputs)
|
...spreadThresholdInputs
|
||||||
});
|
});
|
||||||
ensureToolbarMounted(root, document);
|
ensureToolbarMounted(root, document);
|
||||||
|
|
||||||
@ -249,11 +242,19 @@ export function ensurePluginToolbar(
|
|||||||
exportCustomPagesInput,
|
exportCustomPagesInput,
|
||||||
exportRangeSelect,
|
exportRangeSelect,
|
||||||
exportStatusText,
|
exportStatusText,
|
||||||
root
|
root,
|
||||||
|
spreadFilterFlowTypeSelect,
|
||||||
|
spreadFilterOnlyAssignSelect,
|
||||||
|
spreadFilterRangeSelect,
|
||||||
|
spreadFilterTypeSelect,
|
||||||
|
spreadThresholdInputs
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
spreadFilterTypeSelect.addEventListener("change", () => {
|
spreadFilterTypeSelect.addEventListener("change", () => {
|
||||||
syncSpreadFilterControlState({
|
syncSpreadFilterControlState({
|
||||||
|
audienceProfileByIdExportButton,
|
||||||
|
audienceProfileExportButton,
|
||||||
|
audienceProfileFieldButton,
|
||||||
batchSubmitButton,
|
batchSubmitButton,
|
||||||
exportButton,
|
exportButton,
|
||||||
exportCustomPagesInput,
|
exportCustomPagesInput,
|
||||||
@ -345,6 +346,67 @@ function createSpreadThresholdInput(
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSpreadThresholdControls(
|
||||||
|
document: Document,
|
||||||
|
inputs: Record<keyof SpreadThresholdFilter["thresholds"], HTMLInputElement>
|
||||||
|
): HTMLElement[] {
|
||||||
|
const controls: HTMLElement[] = [];
|
||||||
|
const entries: Array<[string, HTMLInputElement]> = [
|
||||||
|
["评论", inputs.averageCommentCount],
|
||||||
|
["时长", inputs.averageDuration],
|
||||||
|
["点赞", inputs.averageLikeCount],
|
||||||
|
["转发", inputs.averageShareCount],
|
||||||
|
["完播率", inputs.finishRate],
|
||||||
|
["互动率", inputs.interactionRate],
|
||||||
|
["播放中位数", inputs.playMedian]
|
||||||
|
];
|
||||||
|
|
||||||
|
entries.forEach(([label, input], index) => {
|
||||||
|
if (index > 0) {
|
||||||
|
controls.push(createSpreadThresholdConjunction(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = document.createElement("label");
|
||||||
|
wrapper.dataset.pluginSpreadThresholdControl = input.dataset.pluginSpreadThreshold;
|
||||||
|
applySpreadThresholdControlStyles(wrapper);
|
||||||
|
|
||||||
|
const labelText = document.createElement("span");
|
||||||
|
labelText.textContent = label;
|
||||||
|
|
||||||
|
const operator = document.createElement("b");
|
||||||
|
operator.dataset.pluginSpreadThresholdOperator = "gte";
|
||||||
|
operator.textContent = "≥";
|
||||||
|
|
||||||
|
wrapper.append(labelText, operator, input);
|
||||||
|
controls.push(wrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
return controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSpreadThresholdConjunction(document: Document): HTMLElement {
|
||||||
|
const conjunction = document.createElement("span");
|
||||||
|
conjunction.dataset.pluginSpreadThresholdConjunction = "and";
|
||||||
|
conjunction.textContent = "且";
|
||||||
|
applySpreadThresholdConjunctionStyles(conjunction);
|
||||||
|
return conjunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSpreadThresholdRule(document: Document): HTMLElement {
|
||||||
|
const rule = document.createElement("span");
|
||||||
|
rule.dataset.pluginSpreadThresholdRule = "and-gte";
|
||||||
|
applySpreadThresholdRuleStyles(rule);
|
||||||
|
|
||||||
|
const emphasis = document.createElement("strong");
|
||||||
|
emphasis.textContent = "AND";
|
||||||
|
|
||||||
|
const detail = document.createElement("small");
|
||||||
|
detail.textContent = "每项取值 >= 输入值";
|
||||||
|
|
||||||
|
rule.append("全部满足", emphasis, detail);
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
function readSpreadThresholdInputs(
|
function readSpreadThresholdInputs(
|
||||||
root: HTMLElement
|
root: HTMLElement
|
||||||
): Record<keyof SpreadThresholdFilter["thresholds"], HTMLInputElement> {
|
): Record<keyof SpreadThresholdFilter["thresholds"], HTMLInputElement> {
|
||||||
@ -719,8 +781,8 @@ function findNativeActionButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const candidates = Array.from(root.querySelectorAll("button")).filter(
|
const candidates = Array.from(root.querySelectorAll("button")).filter(
|
||||||
(element): element is HTMLElement =>
|
(element): element is HTMLButtonElement =>
|
||||||
element instanceof document.defaultView!.HTMLElement
|
element instanceof document.defaultView!.HTMLButtonElement
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
candidates.find((element) => normalizeText(element.textContent) === text) ?? null
|
candidates.find((element) => normalizeText(element.textContent) === text) ?? null
|
||||||
@ -739,12 +801,12 @@ function applyToolbarRootStyles(root: HTMLElement): void {
|
|||||||
function applyToolbarPanelStyles(panel: HTMLElement): void {
|
function applyToolbarPanelStyles(panel: HTMLElement): void {
|
||||||
panel.style.display = "flex";
|
panel.style.display = "flex";
|
||||||
panel.style.flexDirection = "column";
|
panel.style.flexDirection = "column";
|
||||||
panel.style.alignItems = "stretch";
|
panel.style.alignItems = "center";
|
||||||
panel.style.gap = "6px";
|
panel.style.gap = "6px";
|
||||||
panel.style.flex = "1 1 auto";
|
panel.style.flex = "1 1 auto";
|
||||||
panel.style.minWidth = "0";
|
panel.style.minWidth = "0";
|
||||||
panel.style.padding = "2px 0";
|
panel.style.padding = "0";
|
||||||
panel.style.overflowX = "visible";
|
panel.style.overflowX = "hidden";
|
||||||
panel.style.overflowY = "hidden";
|
panel.style.overflowY = "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -762,41 +824,42 @@ function applyToolbarRowStyles(row: HTMLElement): void {
|
|||||||
function applyToolbarGroupStyles(group: HTMLElement): void {
|
function applyToolbarGroupStyles(group: HTMLElement): void {
|
||||||
group.style.display = "flex";
|
group.style.display = "flex";
|
||||||
group.style.alignItems = "center";
|
group.style.alignItems = "center";
|
||||||
group.style.gap = "6px";
|
group.style.gap = "8px";
|
||||||
group.style.minWidth = "0";
|
group.style.minWidth = "0";
|
||||||
group.style.flex = "0 0 auto";
|
group.style.flex = "0 0 auto";
|
||||||
group.style.flexWrap = "nowrap";
|
group.style.flexWrap = "nowrap";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyThresholdGroupStyles(group: HTMLElement): void {
|
||||||
|
group.style.display = "flex";
|
||||||
|
group.style.alignItems = "center";
|
||||||
|
group.style.gap = "7px";
|
||||||
|
group.style.minWidth = "0";
|
||||||
|
group.style.flex = "1 1 auto";
|
||||||
|
group.style.flexWrap = "nowrap";
|
||||||
|
group.style.overflowX = "auto";
|
||||||
|
group.style.overflowY = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
function createToolbarGroupTitle(document: Document, label: string): HTMLElement {
|
function createToolbarGroupTitle(document: Document, label: string): HTMLElement {
|
||||||
const title = document.createElement("span");
|
const title = document.createElement("span");
|
||||||
title.dataset.pluginToolbarTitle = label;
|
title.dataset.pluginToolbarTitle = label;
|
||||||
title.textContent = label;
|
title.textContent = label;
|
||||||
title.style.color = "#64748b";
|
title.style.display = "flex";
|
||||||
|
title.style.alignItems = "center";
|
||||||
|
title.style.height = "32px";
|
||||||
|
title.style.padding = "0 10px";
|
||||||
|
title.style.border = "1px solid #cfe0ff";
|
||||||
|
title.style.borderRadius = "8px";
|
||||||
|
title.style.background = "#eef5ff";
|
||||||
|
title.style.color = "#2563eb";
|
||||||
title.style.fontSize = "12px";
|
title.style.fontSize = "12px";
|
||||||
title.style.fontWeight = "700";
|
title.style.fontWeight = "900";
|
||||||
title.style.lineHeight = "32px";
|
|
||||||
title.style.flex = "0 0 auto";
|
title.style.flex = "0 0 auto";
|
||||||
title.style.whiteSpace = "nowrap";
|
title.style.whiteSpace = "nowrap";
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyToolbarDividerStyles(divider: HTMLElement): void {
|
|
||||||
divider.style.width = "1px";
|
|
||||||
divider.style.height = "24px";
|
|
||||||
divider.style.background = "#e5e7eb";
|
|
||||||
divider.style.flex = "0 0 auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyToolbarActionStyles(actions: HTMLElement): void {
|
|
||||||
actions.style.display = "flex";
|
|
||||||
actions.style.alignItems = "center";
|
|
||||||
actions.style.gap = "8px";
|
|
||||||
actions.style.flex = "0 0 auto";
|
|
||||||
actions.style.paddingLeft = "10px";
|
|
||||||
actions.style.borderLeft = "1px solid #e5e7eb";
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyNativeControlStyles(
|
function applyNativeControlStyles(
|
||||||
document: Document,
|
document: Document,
|
||||||
controls: {
|
controls: {
|
||||||
@ -876,7 +939,14 @@ function applyNativeControlStyles(
|
|||||||
element.dataset.pluginSpreadThreshold
|
element.dataset.pluginSpreadThreshold
|
||||||
) {
|
) {
|
||||||
element.style.width =
|
element.style.width =
|
||||||
element.dataset.pluginSpreadThreshold === "playMedian" ? "104px" : "78px";
|
element.dataset.pluginSpreadThreshold === "playMedian" ? "82px" : "58px";
|
||||||
|
element.style.minWidth = "0";
|
||||||
|
element.style.height = "26px";
|
||||||
|
element.style.border = "0";
|
||||||
|
element.style.borderRadius = "0";
|
||||||
|
element.style.padding = "0";
|
||||||
|
element.style.outline = "0";
|
||||||
|
element.style.fontWeight = "700";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -896,11 +966,61 @@ function applyPrimaryButtonStyles(
|
|||||||
"background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
|
"background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySpreadThresholdControlStyles(control: HTMLElement): void {
|
||||||
|
control.style.display = "grid";
|
||||||
|
control.style.gridTemplateColumns = "auto auto 1fr";
|
||||||
|
control.style.alignItems = "center";
|
||||||
|
control.style.gap = "6px";
|
||||||
|
control.style.height = "32px";
|
||||||
|
control.style.padding = "0 8px";
|
||||||
|
control.style.border = "1px solid #dbe2ec";
|
||||||
|
control.style.borderRadius = "8px";
|
||||||
|
control.style.background = "#ffffff";
|
||||||
|
control.style.flex = "0 0 auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySpreadThresholdConjunctionStyles(conjunction: HTMLElement): void {
|
||||||
|
conjunction.style.color = "#0f8a5f";
|
||||||
|
conjunction.style.fontSize = "12px";
|
||||||
|
conjunction.style.fontWeight = "900";
|
||||||
|
conjunction.style.whiteSpace = "nowrap";
|
||||||
|
conjunction.style.flex = "0 0 auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySpreadThresholdRuleStyles(rule: HTMLElement): void {
|
||||||
|
rule.style.display = "flex";
|
||||||
|
rule.style.alignItems = "center";
|
||||||
|
rule.style.height = "32px";
|
||||||
|
rule.style.padding = "0 10px";
|
||||||
|
rule.style.border = "1px solid #d9efe7";
|
||||||
|
rule.style.borderRadius = "8px";
|
||||||
|
rule.style.background = "#f0fbf6";
|
||||||
|
rule.style.color = "#0f8a5f";
|
||||||
|
rule.style.fontSize = "12px";
|
||||||
|
rule.style.fontWeight = "900";
|
||||||
|
rule.style.whiteSpace = "nowrap";
|
||||||
|
rule.style.flex = "0 0 auto";
|
||||||
|
|
||||||
|
Array.from(rule.children).forEach((child) => {
|
||||||
|
if (child instanceof rule.ownerDocument.defaultView!.HTMLElement) {
|
||||||
|
child.style.marginLeft = child.tagName === "SMALL" ? "6px" : "3px";
|
||||||
|
if (child.tagName === "SMALL") {
|
||||||
|
child.style.color = "#64748b";
|
||||||
|
child.style.fontSize = "12px";
|
||||||
|
child.style.fontWeight = "700";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function applyStatusStyles(statusText: HTMLElement): void {
|
function applyStatusStyles(statusText: HTMLElement): void {
|
||||||
statusText.style.color = "#64748b";
|
statusText.style.color = "#64748b";
|
||||||
statusText.style.fontSize = "12px";
|
statusText.style.fontSize = "12px";
|
||||||
statusText.style.lineHeight = "20px";
|
statusText.style.lineHeight = "20px";
|
||||||
statusText.style.marginLeft = "4px";
|
statusText.style.marginLeft = "0";
|
||||||
|
statusText.style.flex = "1 1 auto";
|
||||||
|
statusText.style.minWidth = "120px";
|
||||||
|
statusText.style.textAlign = "center";
|
||||||
statusText.style.whiteSpace = "nowrap";
|
statusText.style.whiteSpace = "nowrap";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -949,6 +1069,19 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
|||||||
transform: none !important;
|
transform: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-plugin-spread-threshold-control] span,
|
||||||
|
[data-plugin-spread-threshold-control] b {
|
||||||
|
color: #667085 !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-plugin-spread-threshold-control] b {
|
||||||
|
color: #0f8a5f !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
@ -967,8 +1100,8 @@ function findButtonContainingText(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const candidates = Array.from(root.querySelectorAll("button")).filter(
|
const candidates = Array.from(root.querySelectorAll("button")).filter(
|
||||||
(element): element is HTMLElement =>
|
(element): element is HTMLButtonElement =>
|
||||||
element instanceof document.defaultView!.HTMLElement
|
element instanceof document.defaultView!.HTMLButtonElement
|
||||||
);
|
);
|
||||||
|
|
||||||
return candidates.find((element) => normalizeText(element.textContent).includes(text)) ?? null;
|
return candidates.find((element) => normalizeText(element.textContent).includes(text)) ?? null;
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export const DEFAULT_BATCH_SUBMIT_BASE_URL = "http://192.168.31.21:8083";
|
export const DEFAULT_BATCH_SUBMIT_BASE_URL = "http://localhost:8083";
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { createBatchSubmitClient } from "../src/shared/batch-submit-client";
|
|||||||
|
|
||||||
describe("batch-submit-client", () => {
|
describe("batch-submit-client", () => {
|
||||||
test("exports the default batch submit base url", () => {
|
test("exports the default batch submit base url", () => {
|
||||||
expect(DEFAULT_BATCH_SUBMIT_BASE_URL).toBe("http://192.168.31.21:8083");
|
expect(DEFAULT_BATCH_SUBMIT_BASE_URL).toBe("http://localhost:8083");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("posts the batch payload with a Bearer token", async () => {
|
test("posts the batch payload with a Bearer token", async () => {
|
||||||
|
|||||||
@ -43,7 +43,7 @@ describe("manifest", () => {
|
|||||||
"https://*.xingtu.cn/ad/creator/market*",
|
"https://*.xingtu.cn/ad/creator/market*",
|
||||||
"https://login-api.intelligrow.cn/*",
|
"https://login-api.intelligrow.cn/*",
|
||||||
"https://talent-search.intelligrow.cn/*",
|
"https://talent-search.intelligrow.cn/*",
|
||||||
"http://192.168.31.21:8083/*",
|
"http://localhost:8083/*",
|
||||||
"https://*/*"
|
"https://*/*"
|
||||||
]);
|
]);
|
||||||
expect(releaseManifest.host_permissions).not.toEqual(
|
expect(releaseManifest.host_permissions).not.toEqual(
|
||||||
|
|||||||
@ -9,11 +9,12 @@ const disposers: Array<() => void> = [];
|
|||||||
|
|
||||||
describe("market-content-entry", () => {
|
describe("market-content-entry", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
installUsableLocalStorage();
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
document.documentElement.removeAttribute("data-sces-market-rows");
|
document.documentElement.removeAttribute("data-sces-market-rows");
|
||||||
document.documentElement.removeAttribute("data-sces-market-request-snapshot");
|
document.documentElement.removeAttribute("data-sces-market-request-snapshot");
|
||||||
document.documentElement.removeAttribute("data-test-page-index");
|
document.documentElement.removeAttribute("data-test-page-index");
|
||||||
window.localStorage.clear();
|
clearLocalStorage();
|
||||||
window.history.replaceState({}, "", "/");
|
window.history.replaceState({}, "", "/");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -384,7 +385,7 @@ describe("market-content-entry", () => {
|
|||||||
expect(audienceProfileByIdExportButton?.style.color).toBe("rgb(255, 255, 255)");
|
expect(audienceProfileByIdExportButton?.style.color).toBe("rgb(255, 255, 255)");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps plugin filters in two compact aligned rows", async () => {
|
test("renders the approved compact toolbar layout from the preview", async () => {
|
||||||
document.body.innerHTML = buildMarketFixture();
|
document.body.innerHTML = buildMarketFixture();
|
||||||
|
|
||||||
const { createMarketController } = await import("../src/content/market/index");
|
const { createMarketController } = await import("../src/content/market/index");
|
||||||
@ -405,7 +406,6 @@ describe("market-content-entry", () => {
|
|||||||
const panel = document.querySelector(
|
const panel = document.querySelector(
|
||||||
'[data-plugin-toolbar-panel="root"]'
|
'[data-plugin-toolbar-panel="root"]'
|
||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
const rows = document.querySelectorAll("[data-plugin-toolbar-row]");
|
|
||||||
const primaryRow = document.querySelector(
|
const primaryRow = document.querySelector(
|
||||||
'[data-plugin-toolbar-row="primary"]'
|
'[data-plugin-toolbar-row="primary"]'
|
||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
@ -421,24 +421,56 @@ describe("market-content-entry", () => {
|
|||||||
const thresholdGroup = document.querySelector(
|
const thresholdGroup = document.querySelector(
|
||||||
'[data-plugin-toolbar-group="thresholds"]'
|
'[data-plugin-toolbar-group="thresholds"]'
|
||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
const titles = Array.from(document.querySelectorAll("[data-plugin-toolbar-title]"))
|
const statusText = document.querySelector(
|
||||||
.map((element) => element.textContent);
|
'[data-plugin-export-status="text"]'
|
||||||
|
) as HTMLElement | null;
|
||||||
|
const titles = Array.from(
|
||||||
|
document.querySelectorAll("[data-plugin-toolbar-title]")
|
||||||
|
) as HTMLElement[];
|
||||||
|
const buttons = Array.from(
|
||||||
|
document.querySelectorAll("[data-plugin-toolbar-group='data'] button")
|
||||||
|
) as HTMLButtonElement[];
|
||||||
|
const operators = Array.from(
|
||||||
|
document.querySelectorAll("[data-plugin-spread-threshold-operator]")
|
||||||
|
).map((element) => element.textContent);
|
||||||
|
const conjunctions = Array.from(
|
||||||
|
document.querySelectorAll("[data-plugin-spread-threshold-conjunction]")
|
||||||
|
).map((element) => element.textContent);
|
||||||
|
|
||||||
expect(toolbar?.style.flexWrap).toBe("nowrap");
|
expect(toolbar?.style.flexWrap).toBe("nowrap");
|
||||||
expect(panel?.style.flexDirection).toBe("column");
|
expect(panel?.style.flexDirection).toBe("column");
|
||||||
expect(panel?.style.alignItems).toBe("stretch");
|
expect(panel?.style.alignItems).toBe("center");
|
||||||
expect(panel?.style.padding).toBe("2px 0px");
|
|
||||||
expect(panel?.style.overflowX).toBe("visible");
|
|
||||||
expect(rows).toHaveLength(2);
|
|
||||||
expect(primaryRow?.style.flexWrap).toBe("nowrap");
|
expect(primaryRow?.style.flexWrap).toBe("nowrap");
|
||||||
expect(thresholdRow?.style.flexWrap).toBe("nowrap");
|
expect(thresholdRow?.style.flexWrap).toBe("nowrap");
|
||||||
|
expect(thresholdRow?.style.alignItems).toBe("center");
|
||||||
expect(dataGroup?.parentElement).toBe(primaryRow);
|
expect(dataGroup?.parentElement).toBe(primaryRow);
|
||||||
expect(videoGroup?.parentElement).toBe(primaryRow);
|
expect(videoGroup?.parentElement).toBe(primaryRow);
|
||||||
|
expect(statusText?.parentElement).toBe(primaryRow);
|
||||||
expect(thresholdGroup?.parentElement).toBe(thresholdRow);
|
expect(thresholdGroup?.parentElement).toBe(thresholdRow);
|
||||||
expect(thresholdGroup?.style.flexWrap).toBe("nowrap");
|
expect(thresholdGroup?.style.flexWrap).toBe("nowrap");
|
||||||
|
expect(thresholdGroup?.style.overflowX).toBe("auto");
|
||||||
expect(primaryRow?.style.justifyContent).toBe("flex-start");
|
expect(primaryRow?.style.justifyContent).toBe("flex-start");
|
||||||
expect(thresholdRow?.style.justifyContent).toBe("flex-start");
|
expect(thresholdRow?.style.justifyContent).toBe("flex-start");
|
||||||
expect(titles).toEqual(["达人数据", "视频口径", "传播指标"]);
|
expect(titles.map((element) => element.textContent)).toEqual([
|
||||||
|
"视频口径",
|
||||||
|
"传播指标筛选"
|
||||||
|
]);
|
||||||
|
expect(titles[0]?.style.background).toBe("rgb(238, 245, 255)");
|
||||||
|
expect(titles[1]?.style.background).toBe("rgb(238, 245, 255)");
|
||||||
|
expect(
|
||||||
|
document.querySelector("[data-plugin-spread-threshold-rule]")?.textContent
|
||||||
|
).toContain("全部满足AND");
|
||||||
|
expect(
|
||||||
|
document.querySelector("[data-plugin-spread-threshold-rule]")?.textContent
|
||||||
|
).toContain("每项取值 >= 输入值");
|
||||||
|
expect(operators).toEqual(["≥", "≥", "≥", "≥", "≥", "≥", "≥"]);
|
||||||
|
expect(conjunctions).toEqual(["且", "且", "且", "且", "且", "且"]);
|
||||||
|
expect(buttons.map((button) => button.style.backgroundColor)).toEqual([
|
||||||
|
"rgb(127, 29, 45)",
|
||||||
|
"rgb(127, 29, 45)",
|
||||||
|
"rgb(127, 29, 45)",
|
||||||
|
"rgb(127, 29, 45)"
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("remounts the plugin action bar when the native market action row appears later", async () => {
|
test("remounts the plugin action bar when the native market action row appears later", async () => {
|
||||||
@ -3833,6 +3865,58 @@ describe("market-content-entry", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function clearLocalStorage(): void {
|
||||||
|
if (typeof window.localStorage.clear === "function") {
|
||||||
|
window.localStorage.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = window.localStorage.length - 1; index >= 0; index -= 1) {
|
||||||
|
const key = window.localStorage.key(index);
|
||||||
|
if (key) {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installUsableLocalStorage(): void {
|
||||||
|
if (
|
||||||
|
typeof window.localStorage.getItem === "function" &&
|
||||||
|
typeof window.localStorage.setItem === "function" &&
|
||||||
|
typeof window.localStorage.removeItem === "function" &&
|
||||||
|
typeof window.localStorage.clear === "function"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = new Map<string, string>();
|
||||||
|
const storage: Storage = {
|
||||||
|
get length() {
|
||||||
|
return values.size;
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
values.clear();
|
||||||
|
},
|
||||||
|
getItem(key: string) {
|
||||||
|
return values.get(key) ?? null;
|
||||||
|
},
|
||||||
|
key(index: number) {
|
||||||
|
return Array.from(values.keys())[index] ?? null;
|
||||||
|
},
|
||||||
|
removeItem(key: string) {
|
||||||
|
values.delete(key);
|
||||||
|
},
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
values.set(key, String(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(window, "localStorage", {
|
||||||
|
configurable: true,
|
||||||
|
value: storage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function buildMarketFixture() {
|
function buildMarketFixture() {
|
||||||
return buildMarketPageShell(buildMarketTableOnlyFixture());
|
return buildMarketPageShell(buildMarketTableOnlyFixture());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user