Commit 1949e52d by 何博文

Initial commit

parents
# 资源
需求相关资源或其他信息
# 文档
前后端开发产出
- 开发文档
- 联调反馈
- 注意事项
\ No newline at end of file
# partner-mp 第三点(现有接口)改动文档
# partner-mp 第三点(现有接口)改动文档
> 配套:`api-doc.md` §三「现有接口清单」。本文记录本批次(分配03)对**第三点现有接口**的改动。
> 原则:**需要改的才改**;明确废弃的打 `@Deprecated`。改动位置:`hro/hro_spview/services/partner_api.groovy`、`models/partner/partner_users.json`。
> 完成日期:2026-07-03
---
## 〇、本次改动总览
| 项 | 对象 | 类型 | 结论 |
|----|------|------|------|
| 1 | `customer_collect_list` | 接口增强 | 列表项**新增归属来源** `source`/`source_type` |
| 2 | `partner_users.create_type` | 元数据修正 | 字段 label/注释补全枚举值 3 |
| 3 | 3.1/3.2/3.5/3.6/3.7 | 核对 | 无需改动(详见末节) |
| 4 | 废弃接口 | 已处理 | 打 `@Deprecated`(详见末节) |
调用方式同 api-doc.md:`sdk.domainServicePost("接口名", { data }, false, "hro_spview", "partner_api")`
---
## 一、customer_collect_list — 我的客户列表(新增"归属来源")
- **页面/位置**:02 · **P2 我的客户列表**(每个客户卡片);02 · **P3 客户详情**汇总沿用同接口。
- **用途效果**:给"我的客户"列表每个客户**新增归属来源标记**——取该客户**所属合伙人的入驻方式**`partner_users.create_type`)。
- 场景意义:**对接人**视角下该接口跨其名下多个合伙人聚合客户,用 `source` 区分每个客户来自"介绍人邀请 / 自主申请 / 介绍人后台添加"的合伙人;合伙人本人视角下同一值。
- 来源口径以 `partner_users.create_type` 为准,枚举:`1介绍人邀请 / 2自主申请 / 3介绍人后台添加`
- **入参****未变**,沿用原有):
| 字段 | 类型 | 说明 |
|------|------|------|
| user_combo | string | 姓名/手机号搜索 |
| deal_date | string | 成交时间筛选 `yyyy-MM` |
| UserMemberId | long | 精确查某客户(详情场景) |
| page / page_size | int | 分页 |
- **反参**:外层 `{ code, msg, data:{ rc_count, page, data_list[] } }``data_list[]` **在原有字段基础上新增 2 个**
| 字段 | 类型 | 新增? | 说明 |
|------|------|------|------|
| **source** | string | ✅ 新增 | 归属来源显示名:"介绍人邀请"/"自主申请"/"介绍人后台添加";无归属合伙人时 `"-"` |
| **source_type** | string | ✅ 新增 | 归属来源码:"1"/"2"/"3";无则 `""` |
| customer_name / customer_mobile | string | 原有 | 客户名 / 手机 |
| UserMemberId / UserType | - | 原有 | 客户id / 类型(1个人/2企业) |
| order_count / order_amount / order_commission | - | 原有 | 成交笔数 / 金额 / 佣金 |
| intention_products | string | 原有 | 意向产品 |
| companyName / last_order_createDate | - | 原有 | 企业名 / 最近成交时间 |
- **示例**
```jsonc
// 请求 data: { "page": 1, "page_size": 10 }
// 响应(截取一个客户,★为本次新增)
{ "code": 0, "msg": "成功", "data": {
"rc_count": 23, "page": 1,
"data_list": [{
"UserMemberId": 100234, "UserType": "2",
"customer_name": "杭州某某科技有限公司", "customer_mobile": "13800001111",
"companyName": "杭州某某科技有限公司",
"order_count": 2, "order_amount": "40000.00", "order_commission": "1200.00",
"intention_products": "岗位外包,薪酬代发",
"source": "介绍人邀请", // ★ 归属合伙人的入驻方式
"source_type": "1" // ★ 对应码
}]
}}
```
- **前端接入提示**:P2 客户卡片可加一枚"来源"标签展示 `source`;若做来源筛选,用 `source_type`(1/2/3) 传参需后端再加筛选参数(本次未加筛选,仅出参)。
---
## 二、partner_users.create_type — 入驻方式枚举(元数据修正)
- **页面/位置**:贯穿 01 P4 申请合伙人、后台合伙人管理、以及上文 `source` 来源。
- **改动**`create_type` 字段的 label/注释由"1介绍人邀请/2自主申请"**补全为"1介绍人邀请/2自主申请/3介绍人后台添加"**(映射 `create_type_mapping` 本就含 3 值,此前 label 漏写)。
- **枚举权威**
| create_type | 含义 | 产生场景 |
|-------------|------|---------|
| 1 | 介绍人邀请 | 邀请链接/扫码进入申请,带介绍人(`contactor_uniplat_uid`),入驻后状态直接=正常 |
| 2 | 自主申请 | 用户自行提交申请(01 P4),待审核 |
| 3 | 介绍人后台添加 | 渠道管理员/后台代为添加 |
- **无接口出入参变化**,仅元数据;`join_partner`(小程序端申请)仍只接受 `create_type` 1/2,值 3 由后台路径产生。
---
## 三、核对未改动的接口(§三 其余子节结论)
| §三 子节 | ⚠️ 点 | 结论 | 说明 |
|------|------|------|------|
| 3.1 登录鉴权 `user_info` | 补 is_partner/is_contactor/is_introducer | **已具备** | is_partner/is_contactor 原有;`is_introducer` 已由第四点(分配02)补上 |
| 3.2 申请合伙人 `join_partner` | 介绍人字段 | **已具备** | `contactor_uniplat_uid` 承载介绍人;create_type=1 时必填 |
| 3.5 我的佣金 `get_deal_settlement` | 与对接人佣金口径 | **保持独立** | 合伙人=可提现余额模型;对接人=次月工资无提现(第四点 `contactor_commission_*` 另建)。两者本就是不同模型,无需合并 |
| 3.6 产品 | — | 不改 | api-doc 明确无需改动 |
| 3.7 海报 | — | 不改 | 见下"废弃候选" |
---
## 四、废弃接口
废弃接口:recommend_list 、get_qrcode_image_with_poster
# partner-mp 第四点接口文档(介绍人视图 03 + 对接人视图 04)
# partner-mp 第四点接口文档(介绍人视图 03 + 对接人视图 04)
> 配套:`api-doc.md`(现状梳理 + 第四点缺口清单)。本文是第四点**已实现接口**的对接说明。
> 实现位置:`hro/hro_spview/services/partner_api.groovy`(领域服务)。写入/流转复用 `models/partner/partner_opportunity` 模型公共方法。
> 完成日期:2026-07-03
---
## 〇、通用约定
### 调用方式(领域服务)
```ts
sdk.domainServicePost(
"接口名", // 见下方各接口
{ data: {入参...} }, // 入参全部放在 data(后端用 ctx.getBody 读取)
false, // isAnonymous=false,需登录
"hro_spview", // subProjectName
"partner_api" // serviceName
)
```
- **路由**`POST /general/project/hro_spview/service/partner_api/{接口名}`
- **认证**:全部接口需登录态;后端以当前登录 `uid` 做数据范围过滤(对接人=本人分配的商机/订单,介绍人=本人推荐的合伙人)。无匿名接口。
- **越权保护**:详情类接口(商机详情/订单详情/合伙人详情)后端校验归属,非本人数据返回"无权查看"。
### 响应外层两种形态
| 形态 | 结构 | 用到的接口 |
|------|------|-----------|
| ApiResult 包裹 | `{ code:0, msg:"成功", data:{...} }` | 列表类:②⑥⑨⑫⑮ |
| 直接返回对象 | `{...}`(即下文"反参"字段直接在顶层) | 概览/详情/操作类:①③④⑤⑦⑧⑩⑪⑬⑭⑯⑰ |
> 下文"反参"均指**业务数据体**(ApiResult 形态时即 `data` 内容)。分页统一:`page` 从 1 开始,`page_size` 默认 10、上限 100。金额字段为字符串化小数(如 `"50.00"`)。
### 商机状态码 / 来源码
- `status`:1待审核 / 2待分配 / 3跟进中 / 4暂时挂起 / 5已成单 / 6已作废
- `source`:1合伙人添加 / 2落地页扫码 / 3后台添加
- 结算 `status`:0待审核 / 1已审核(对接人=次月随工资发放,视为已发放)/ 2已取消 / 3已审核待入账 / 4未设置对接人
---
# 一、对接人视图(04)P0 · 接口 ①-⑬
## ① contactor_home_summary — 对接人首页三宫格
- **页面/位置**:04 · P1 首页顶部三宫格。
- **用途效果**:进首页即拉取,展示"跟进中商机数 / 我负责产品数 / 本月佣金"。
- **入参**:无(取当前登录对接人)。
- **反参**
| 字段 | 类型 | 说明 |
|------|------|------|
| following_count | int | 跟进中(status=3)商机数 |
| my_product_count | int | 我负责的产品数(产品主责) |
| month_commission | number | 本月佣金合计(结算表 RoleType=3 本月、排除已取消) |
- **示例**
```jsonc
// 请求 data: {}
// 响应
{ "following_count": 8, "my_product_count": 3, "month_commission": "1200.00" }
```
## ② opportunity_list — 商机列表
- **页面/位置**:04 · P2 商机列表(tab:跟进中/暂时挂起/已成单/已作废)+ 顶部筛选。
- **用途效果**:按 tab(status)与筛选分页拉取"分配给我"的商机;跟进中 tab 可用 `following_untracked_count` 显示未跟进红点。
- **入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| status | int | 否 | tab:3/4/5/6;不传=全部 |
| source | int | 否 | 来源:1/2/3 |
| product_ids | int[] | 否 | 我负责产品多选过滤 |
| date_from / date_to | string | 否 | 创建时间范围 `yyyy-MM-dd` |
| partner_key | string | 否 | 合伙人姓名/手机搜索 |
| page / page_size | int | 否 | 分页 |
- **反参**(ApiResult.data):`total``page``following_untracked_count`(跟进中且>3天未更新数)、`data_list[]`
| 字段 | 说明 |
|------|------|
| opportunity_id / code | 商机id / 编号(LMxxx) |
| customer_name | 客户(企业)名 |
| customer_attr | 产品分类tag(大类-分类) |
| product_name | 产品名 |
| partner_name / channel_admin_name | 合伙人 / 渠道管理员 |
| source / source_code | 来源显示 / 码 |
| create_time / update_time | 创建/更新时间 |
| status / status_code | 状态显示 / 码 |
- **示例**
```jsonc
// 请求 data: { "status": 3, "product_ids": [101], "page": 1, "page_size": 10 }
// 响应
{ "code": 0, "msg": "成功", "data": {
"total": 8, "page": 1, "following_untracked_count": 2,
"data_list": [{
"opportunity_id": 5001, "code": "LM20260703091500123",
"customer_name": "杭州某某科技有限公司", "customer_attr": "用工合规-异地用工",
"product_name": "岗位外包", "partner_name": "张三", "channel_admin_name": "李管理",
"source": "后台添加", "source_code": 3,
"create_time": "2026-07-01 09:15:00", "update_time": "2026-07-02 10:00:00",
"status": "跟进中", "status_code": 3
}]
}}
```
## ③ opportunity_detail — 商机详情
- **页面/位置**:04 · P4 商机详情(P2 列表项点击进入)。
- **用途效果**:展示客户信息 + 四方干系人 + 状态流转记录。**仅本人分配的商机可看**(否则"无权查看该商机")。
- **入参**`{ opportunity_id: long }`
- **反参**
| 字段 | 说明 |
|------|------|
| opportunity_id / code / status / status_code | 基本信息 |
| customer | 客户信息对象:customer_name, customer_attr, contact_name, contact_mobile, product_name, product_category, source, description |
| stakeholders[] | 四方干系人:`{role, name, mobile}`;对接人本人附 `is_self:true` |
| logs[] | 状态流转记录(倒序):`{time, title, desc, operator}` |
| days_since_update | 距上次更新天数 |
| overdue_tip | 逾期提示文案(纯展示) |
- **示例**
```jsonc
// 请求 data: { "opportunity_id": 5001 }
// 响应
{
"opportunity_id": 5001, "code": "LM20260703091500123", "status": "跟进中", "status_code": 3,
"customer": { "customer_name": "杭州某某科技有限公司", "customer_attr": "用工合规-异地用工",
"contact_name": "王经理", "contact_mobile": "13800001111", "product_name": "岗位外包",
"product_category": "用工合规-异地用工", "source": "后台添加", "description": "有50人用工需求" },
"stakeholders": [
{ "role": "介绍人", "name": "赵推荐", "mobile": "138****2222" },
{ "role": "合伙人", "name": "张三", "mobile": "138****3333" },
{ "role": "渠道管理员", "name": "李管理", "mobile": "138****4444" },
{ "role": "对接人", "name": "本人", "mobile": "138****5555", "is_self": true }
],
"logs": [ { "time": "2026-07-02 10:00", "title": "状态流转:待分配 → 跟进中", "desc": "", "operator": "李管理" } ],
"days_since_update": 1, "overdue_tip": "挂起超30天将自动作废(示例文案)"
}
```
## ④ opportunity_transfer — 商机流转(挂起/作废/标记成单)
- **页面/位置**:04 · P4 详情底部操作、P5 挂起弹窗、P14 作废弹窗、详情"标记成单"。
- **用途效果**:对本人商机做状态流转。**仅本人商机可操作**;标记成单只改状态、不生成订单(订单由财务真实成交自动落)。
- **入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| opportunity_id | long | 是 | 商机id |
| target_status | int | 是 | 4挂起 / 6作废 / 5成单(合法流转:3→4/5/6,4→3见下) |
| reason | string | 挂起/作废必填 | 原因(单选文案) |
| remark | string | 否 | 备注(作废选"其他"时应填) |
> 合法流转:跟进中(3)→挂起(4)/成单(5)/作废(6);挂起(4)→跟进中(3)恢复/作废(6)。作废不可逆。
- **反参**`{ result:0, msg:"操作成功", opportunity_id }`
- **示例**
```jsonc
// 挂起 data: { "opportunity_id": 5001, "target_status": 4, "reason": "客户预算暂未确认" }
// 成单 data: { "opportunity_id": 5001, "target_status": 5 }
// 响应
{ "result": 0, "msg": "操作成功", "opportunity_id": 5001 }
```
## ⑤ opportunity_create — 新增商机 / 对接人代录入
- **页面/位置**:04 · P6 新增商机表单(P2 列表右下角 FAB 进入)。
- **用途效果**:对接人代客户录入商机。多选产品拆成多条商机;企业名 free-text(不预先建企业档案);合伙人可空=直销。命中去重则刷新原商机更新时间不新建;按分配表(企业名+产品)自动路由对接人,未命中转待分配。
- **入参**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| customer_name | string | 是 | 企业名称(完整) |
| product_ids | int[] | 是 | 意向产品,多选拆多商机 |
| contact_name | string | 否 | 客户联系人 |
| contact_mobile | string | 否 | 联系人手机 |
| partner_id | long | 否 | 关联合伙人(partner_users.id);不传=直销 |
| description | string | 否 | 需求描述 |
> 去重口径:同 `企业名 + 产品 + 进行中(待审核/待分配/跟进中/挂起)` 已存在则不新建。
- **反参**`{ result:0, msg:"提交成功", items:[{ product_id, product_name, result:'created'|'duplicated', opportunity_id }] }`
- **示例**
```jsonc
// 请求 data: { "customer_name":"杭州某某科技有限公司", "product_ids":[101,102],
// "contact_name":"王经理", "contact_mobile":"13800001111", "partner_id": 88 }
// 响应
{ "result": 0, "msg": "提交成功", "items": [
{ "product_id": 101, "product_name": "岗位外包", "result": "created", "opportunity_id": 5002 },
{ "product_id": 102, "product_name": "薪酬代发", "result": "duplicated", "opportunity_id": 4900 }
]}
```
## ⑥ order_list — 订单列表
- **页面/位置**:04 · P7 订单列表(月份切换:本月/上月/全部)。
- **用途效果**:展示对接人本人分佣的订单(结算表 RoleType=3 且 RoleUid=本人、佣金≠0)。
- **入参**`{ month: "this"|"last"|"all"(默认), page, page_size }`
- **反参**(ApiResult.data):`total``page``data_list[]``order_id, customer_name, customer_attr, product_name, doc_no(订单号), amount(订单金额), commission(本人佣金), status_code, status`
- **示例**
```jsonc
// 请求 data: { "month": "this", "page": 1, "page_size": 10 }
// 响应
{ "code":0, "msg":"成功", "data": { "total": 5, "page": 1, "data_list": [{
"order_id": 90001, "customer_name": "杭州某某科技有限公司", "customer_attr": "用工合规-异地用工",
"product_name": "岗位外包", "doc_no": "XB20260601001", "amount": "20000.00",
"commission": "600.00", "status_code": 1, "status": "已审核" }] }}
```
## ⑦ order_detail — 订单详情
- **页面/位置**:04 · P8 订单详情(P7 点击进入)。
- **用途效果**:订单信息 + 我的佣金 + 分佣干系人。**金额权限**:只有对接人本人下发实际佣金;介绍人/合伙人/渠道管理员只给分佣规则文案,响应里不含其金额。**仅本人订单可看**
- **入参**`{ order_id: long }`(结算表行id,即列表 order_id)
- **反参**
| 字段 | 说明 |
|------|------|
| order_id, doc_no, customer_name, customer_attr, product_name | 订单信息 |
| amount, service_type, create_date | 金额/业务类型/日期 |
| my_commission | 我的佣金(本人实际金额) |
| stakeholders[] | 分佣干系人:本人 `{role,name,is_self:true,commission}`;其他 `{role,name,is_self:false,fee_rule_desc}`(无金额) |
- **示例**
```jsonc
// 请求 data: { "order_id": 90001 }
// 响应
{ "order_id": 90001, "doc_no": "XB20260601001", "customer_name": "杭州某某科技有限公司",
"customer_attr": "用工合规-异地用工", "product_name": "岗位外包", "amount": "20000.00",
"service_type": "亲亲企服", "create_date": "2026-06-01", "my_commission": "600.00",
"stakeholders": [
{ "role": "介绍人", "name": "-", "fee_rule_desc": "推荐奖励 ¥50/人(首单成交结算)", "is_self": false },
{ "role": "合伙人", "name": "张三", "fee_rule_desc": "比例 3%(成交额) 续费", "is_self": false },
{ "role": "渠道管理员", "name": "李管理", "fee_rule_desc": "固定 ¥200 一次性", "is_self": false },
{ "role": "对接人", "name": "本人", "is_self": true, "commission": "600.00" }
]}
```
## ⑧ contactor_commission_summary — 对接人佣金三宫格
- **页面/位置**:04 · P9 我的佣金页顶部三宫格。
- **用途效果**:展示"本月待结算 / 已结算订单数 / 累计已发放"。对接人为次月随工资发放、无提现。
- **入参**:无。
- **反参**`month_pending`(本月待审核佣金¥)、`settled_order_count`(已审核订单数)、`total_granted`(累计已审核佣金¥)、`settle_tip`("次月随工资发放,无提现")。
- **示例**
```jsonc
{ "month_pending": "600.00", "settled_order_count": 12, "total_granted": "8400.00", "settle_tip": "次月随工资发放,无提现" }
```
## ⑨ contactor_commission_list — 对接人佣金明细
- **页面/位置**:04 · P9 我的佣金页明细列表。
- **用途效果**:卡片字段与订单列表一致,点击进 ⑦ 订单详情。
- **入参**`{ page, page_size }`
- **反参**:同 ⑥(ApiResult.data:total/page/data_list[])。
## ⑩ my_product_list — 我负责的产品列表
- **页面/位置**:04 · P10 我负责的产品页。
- **用途效果**:主责产品列表 + 顶部汇总;实时聚合商机/订单统计。
- **入参**:无。
- **反参**`summary:{product_count, following_count, month_commission}``data_list[]``product_id, name, category, price_desc, rate_desc(对接人返佣文案), following_count(该产品我在跟商机数), done_count(我成交订单数), month_commission`
- **示例**
```jsonc
{ "summary": { "product_count": 3, "following_count": 8, "month_commission": "1200.00" },
"data_list": [{ "product_id": 101, "name": "岗位外包", "category": "用工合规-异地用工",
"price_desc": "面议", "rate_desc": "比例 2%(成交额) 续费",
"following_count": 5, "done_count": 3, "month_commission": "600.00" }] }
```
## ⑪ my_product_detail — 我负责的产品详情
- **页面/位置**:04 · P11 产品详情(P10 点击进入)。
- **用途效果**:产品信息 + 我的业务四宫格 + 跟进中商机预览(点击进 ③ 商机详情)+ 对接人佣金规则。
- **入参**`{ product_id: long }`
- **反参**`product_id, name, category, price_desc, intro, rate_desc``stat:{following_count, done_count, month_commission, total_commission}``following_opportunities[]`(同 ② 列表项,最多 20 条)。
## ⑫ others_product_list — 其他产品 / 转介绍
- **页面/位置**:04 · P12 其他产品列表。
- **用途效果**:公司全量产品(排除本人主责),供转介绍;附产品负责人 + 对接人佣金规则文案。
- **入参**`{ category?, search_key?, page, page_size }`
- **反参**(ApiResult.data):`total, page, data_list[]``product_id, name, category, price_desc, owner_name(业务负责人), rate_desc`
## ⑬ others_product_detail — 其他产品详情 / 推广物料
- **页面/位置**:04 · P13 其他产品详情/推广物料。
- **用途效果**:产品图文 + 佣金规则大卡(对接人比例+"次月工资结算")+ 产品负责人卡 + 推广物料。海报生成复用现有 `gen_qrcode_code`/`get_qrcode_image`(本人专属推广码,客户扫码经 `bind_contactor_for_login` 沉淀本人名下商机)。
- **入参**`{ product_id: long }`
- **反参**`product_id, name, category, price_desc, intro, rate_desc, commission_rule(三角色规则文案), settle_tip, owner_name, owner_mobile, materials[]{content_type, content, description}`
---
# 二、介绍人视图(03)P1 · 接口 ⑭-⑯
> 口径约束:介绍人视图只展示"合伙人→企业转化漏斗",**不暴露任何订单/佣金金额**(那属合伙人本人与对接人视角)。介绍人奖励为固定 ¥50/人、首单成交结算、渠道管理员线下发放,本端只读展示。
## ⑭ introducer_home_summary — 介绍人首页三宫格
- **页面/位置**:03 · P1 首页介绍人板块三宫格。
- **用途效果**:展示"介绍合伙人数 / 推荐企业数 / 待领奖励(金额+笔数)"。身份判定:曾推荐≥1位合伙人即为介绍人。
- **入参**:无。
- **反参**`partner_count`(我介绍的合伙人数)、`customer_count`(这些合伙人推荐的去重企业数)、`pending_amount`(待领奖励¥)、`pending_count`(待领笔数)。
- **示例**
```jsonc
{ "partner_count": 6, "customer_count": 15, "pending_amount": "100.00", "pending_count": 2 }
```
## ⑮ introducer_partner_list — 我介绍的合伙人列表
- **页面/位置**:03 · P2 介绍人业务详情页。
- **用途效果**:顶部汇总 + 合伙人卡片;手机脱敏;reward_status 取合伙人的介绍奖励状态。
- **入参**`{ page, page_size }`
- **反参**(ApiResult.data):
| 字段 | 说明 |
|------|------|
| summary | `{ partner_count, deal_count(有成单的合伙人数), no_deal_count, reward_total(累计奖励), reward_pending(待发放¥), reward_pending_count }` |
| page / total | 分页 |
| data_list[] | `partner_id, name, avatar(暂空), mobile(脱敏), join_date, customer_count(推荐企业数), deal_count(已成单企业数), reward_status("无奖励"/"待发放"/"已发放")` |
- **示例**
```jsonc
{ "code":0, "msg":"成功", "data": {
"summary": { "partner_count": 6, "deal_count": 4, "no_deal_count": 2,
"reward_total": "200.00", "reward_pending": "100.00", "reward_pending_count": 2 },
"page": 1, "total": 6,
"data_list": [{ "partner_id": 88, "name": "张三", "avatar": "", "mobile": "138****3333",
"join_date": "2026-05-01 10:00:00", "customer_count": 3, "deal_count": 2, "reward_status": "已发放" }]
}}
```
## ⑯ introducer_partner_detail — 合伙人详情(介绍人视角)
- **页面/位置**:03 · P3 合伙人详情页(P2 点击进入)。
- **用途效果**:头部转化数据 + 我获得的奖励 + 推荐企业列表(前端按 全部/已成单/未成单 tab 用 `is_deal` 过滤;已成单可跳商机详情、附 deal_date;未成单附 settled_days 已入驻天数)。**仅本人介绍的合伙人可看**
- **入参**`{ partner_id: long }`(partner_users.id)
- **反参**
| 字段 | 说明 |
|------|------|
| partner_id, name, mobile(脱敏), join_date, join_days | 头部 |
| customer_count, deal_count | 推荐企业数 / 已成单企业数 |
| reward | `{ has_reward, reward_amount, reward_status, trigger_customer(首单触发企业), reward_date, grant_note }`;无奖励时 `{has_reward:false, desc}` |
| customers[] | 推荐企业:`{opportunity_id, customer_name, contact_name, contact_mobile, invite_date, is_deal, deal_date, settled_days}` |
- **示例**
```jsonc
// 请求 data: { "partner_id": 88 }
// 响应
{ "partner_id": 88, "name": "张三", "mobile": "138****3333",
"join_date": "2026-05-01 10:00:00", "join_days": 63, "customer_count": 3, "deal_count": 2,
"reward": { "has_reward": true, "reward_amount": "50.00", "reward_status": "已发放",
"trigger_customer": "杭州某某科技有限公司", "reward_date": "2026-06-05 14:00", "grant_note": "由渠道管理员线下发放" },
"customers": [
{ "opportunity_id": 5001, "customer_name": "杭州某某科技有限公司", "contact_name": "王经理",
"contact_mobile": "13800001111", "invite_date": "2026-05-10 09:00:00", "is_deal": true,
"deal_date": "2026-06-01 10:00:00", "settled_days": 0 },
{ "opportunity_id": 5010, "customer_name": "宁波某某公司", "contact_name": "陈总",
"contact_mobile": "13900002222", "invite_date": "2026-06-20 09:00:00", "is_deal": false,
"deal_date": "", "settled_days": 13 }
]}
```
---
# 三、跨模块补充
## ⑰ user_info 新增字段 is_introducer
- **页面/位置**:03/04 首页判断是否展示"介绍人板块"。
- **用途效果**:现有 `user_info` 接口响应**新增** `is_introducer` 字段。判定:当前用户名下存在≥1位其推荐的合伙人(`partner_users.contactor_uniplat_uid = 当前uid`)即为 true。`is_partner`/`is_contactor` 为现有字段。
- **入参**:无(沿用现有 user_info)。
- **反参(新增片段)**
```jsonc
{ "is_partner": false, "is_contactor": true, "is_introducer": true, "...": "其余字段不变" }
```
---
## 附:数据来源与依赖(供排查)
- 商机:`partner_opportunity`(数据范围按 `contactor_uid`)。
- 分配表:`partner_contactor_assignment`(自动路由/带入对接人,管理后台维护)。
- 主责产品:`partner_product_contactor`(product_id + uid)。
- 订单/佣金:`Partners_Deal_Settlement`(RoleType=3 对接人;由财务真实成交经 `deal_settle_handler` 自动落单、`review_role` 审核)。
- 介绍人奖励:`partner_introducer_reward`(introducer_uid = 介绍人;首单成交由 `createIntroducerReward` 生成待发放记录)。
- 介绍人关系:`partner_users.contactor_uniplat_uid`(= 推荐该合伙人的介绍人 uid)。
# partner-mp 接口说明文档
# partner-mp 接口说明文档
> 面向:接手本项目的后端同事
> 依据:`design-export/01-登录及申请.dc.html`、`02-合伙人.dc.html`、`03-介绍人视图.dc.html`、`04-对接人视图.dc.html` 四份交互原型 + 现有前端代码(`packages/partner-mp/src`)
> 说明:四份原型是**交互原型**,不是视觉设计稿;本文档只关心页面需要什么数据、什么接口,不涉及颜色/间距等视觉内容。
---
## 一、身份体系说明
项目里有三种业务身份,彼此**独立、可叠加**,不是互斥的单选状态(除了下面标注的一组互斥关系):
| 身份 | 判定方式 | 与其他身份的关系 |
| ---------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **普通用户** | 登录但不满足任何角色条件 | 基础态 |
| **合伙人**`is_partner`) | 通过 P4 表单申请,后台审核通过 | 与"对接人"**互斥**:一个用户不会同时是合伙人又是对接人 |
| **对接人**`is_contactor`) | 由后台/渠道管理员指派,员工身份 | 与"合伙人"**互斥**;对接人默认同时拥有"介绍人"资格(见下) |
| **介绍人**`isIntroducer`) | 推荐/邀请他人成为合伙人达成一定条件后自动获得 | **独立叠加**在"合伙人"或"对接人"之上,不互斥;也可能是仅有介绍人身份、既非合伙人也非对接人的用户(01 原型的 P3a→P3b) |
**⚠️ 需要后端确认的关键点**:介绍人判定条件在不同原型文件里描述不一致,见「五、待确认疑点清单」第 1 条。
**当前前端代码的身份判定**[services/type.ts](../src/services/type.ts)):
```ts
isIntroducer = computed(() => !!userInfo2.is_introducer) // 后端字段占位,尚未接入真实判定
is_partner / is_contactor // 直接读 userInfo2 对应字段,后端已有
```
**显示优先级**(来自 01/02 原型逻辑注释):合伙人 / 对接人(主身份,互斥) > 介绍人(可叠加的副身份)。
**核心业务概念**(贯穿多个模块,建议后端建模时注意):
- **客户** vs **商机**:客户是"人/企业"维度(一个客户可能有多条历史记录);商机是"客户 × 产品"维度,一个客户可以对应多条商机(比如同时对多个产品感兴趣)。02 原型 P2(我的客户)和 P8(推荐线索)是两个不同维度的列表,不要混用同一张表。
- **商机状态机**`待审核 → 待分配 → 跟进中 → {已成单 / 已作废 / 暂时挂起}`,其中"暂时挂起"可以退回"跟进中"。前端 UI 会把这 6 个状态聚合展示为更少的 tab(比如 02 的合伙人视角聚合成"全部/进行中/已成单/已作废"4 个 tab;04 的对接人视角聚合成"跟进中/暂时挂起/已成单/已作废"4 个 tab)。
- **对接人分配表**:02/04 两份原型都反复提到"新增商机时查『对接人分配表』(客户+产品命中)→ 命中则带入历史对接人/渠道管理员,未命中由渠道管理员人工分配"。这张表目前前端代码里完全看不到痕迹,需要和后端确认是否已存在、维护入口在哪。
- **佣金计算两种方式**:比例(成交额 × 百分比)和固定金额,具体用哪种由产品配置决定,前端只是展示 `RuleSnapshot` 解析结果。
---
## 二、接口形式说明
项目里的接口分两种形式,调用方式和语义不同:
### 1. uniplat 模型接口(`sdk.core.model(...)`)
调用某个 uniplat 低代码平台上配置好的"模型",可以是:
- `.detail(id, "视图名").query()` — 查详情
- `.list("视图名").query()` — 查列表(也可以走 [sdkList 组件](../src/components/sdkList/index.ts) 封装)
- `.action("动作名").addInputs_parameter(data).execute()` — 执行一个动作(新增/提交表单类操作)
这类接口的字段结构由 uniplat 后台模型配置决定,前端通过 `predict` 对象把原始行数据 `metaRow` 映射成业务字段(例如 `productPredict`),后端同事如果要改字段,通常是在 uniplat 后台模型配置里加字段,不一定要动接口代码。
### 2. 领域服务接口(`sdk.domainServicePost` / `domainServiceGet`)
调用后端自定义的具名接口,形如:
```ts
sdk.domainServicePost("接口名", { data, params }, isAnonymous, subProjectName, serviceName)
```
第一个参数就是接口名(后端路由标识),`subProjectName`/`serviceName` 决定打到哪个后端子服务。项目里出现过的组合:
- `hro_spview` / `partner_api` — 主要业务接口(客户、产品、登录、加入合伙人等)
- `welfare_bean` / `reward_account_api` — 佣金/提现相关接口
- `hro_spview` / `ConfigCitySetting` — 城市配置
这类接口字段结构完全由后端自定义决定,本文档里列出的请求/响应字段是**从现有前端代码里实际读取的字段**,不代表后端接口的完整字段清单。
---
## 三、现有接口清单(按 01/02 原型涉及的业务模块分组)
### 3.1 登录鉴权(对应 01 · P1/P2)
| 接口名 | 形式 | 代码位置 | 用途 |
| ---------------------------------------------------- | -------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `loginWithWechat` | domain service | [wechat-login.ts:120](../src/modules/passport/wechat-login.ts#L120) | 微信授权登录,请求 `{code, appid, client_id}` |
| `check_miniapp_is_bind` | domain service | [wechat-login.ts:259](../src/modules/passport/wechat-login.ts#L259) | 判断当前小程序 openid 是否已绑定账号,响应 `{userid, username}` |
| `bind_phone` | domain service | [wechat-login.ts:281](../src/modules/passport/wechat-login.ts#L281) | 绑定手机号,请求 `{appid, code, client_id, encryptedData, iv}` |
| `bind_miniapp` / `bind` | domain service | [wechat-login.ts:166](../src/modules/passport/wechat-login.ts#L166) | 换绑小程序账号 |
| `user_info` | domain service | [services/common.ts:123](../src/services/common.ts#L123) | 登录后轮询获取用户信息,直到 `r.user_info` 存在或 `code===1099`(账号封禁) |
| `refresh_login` | domain service | [passport/login.ts:475](../src/modules/passport/login.ts#L475) | 刷新登录态 |
| `uniplat_base/system.user``info` / `citizen_info` | uniplat model | [store/user-info.ts:70](../src/store/user-info.ts#L70) | 获取用户信息 `UserInfo``sub, uid, name, realname, mobile, avatar...`)与实名信息 `UserVerifyInfo``citizen_no, citizen_verified, realname...`) |
**⚠️ 本次新需求是否需要改动**`UserInfo` 需要补充/确认 `is_partner``is_contactor``is_introducer` 三个身份字段是否已在响应里返回(当前前端代码里 `isIntroducer` 是占位实现,见「五、待确认疑点」第 2 条)。
### 3.2 申请成为合伙人(对应 01 · P4)
| 接口名 | 形式 | 代码位置 | 请求字段 | 用途 |
| ------------------------------------------------ | -------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `partner_users` / `join_partner`(查询表单配置) | uniplat model action | [more/components/index.ts:65](../src/pages/subpackages/more/components/index.ts#L65) | — | 获取 `role_kind`(身份类别:HR/财务/老板/猎头/博主/其他)下拉选项 |
| `partner_users` / `join_partner`(提交) | uniplat model action | [more/join.vue:203](../src/pages/subpackages/more/join.vue#L203) | `{name, id_card, mobile, contactor_uniplat_uid, role_kind, city, create_type:"2"}` | 提交合伙人申请 |
| `city_list` | domain service | [more/components/index.ts:41](../src/pages/subpackages/more/components/index.ts#L41) | — | 表单里的城市选择器数据源 |
**⚠️ 本次新需求是否需要改动**:01 原型 P4 逻辑注释提到"介绍人"字段(选填,提交后由渠道管理员匹配绑定,或来自邀请链接自动带入并锁定)——当前 `join_partner` 提交参数里没有看到介绍人相关字段,需要确认是否要新增 `introducer_uid` 之类的参数。身份证号是否强制必填也有矛盾(见待确认疑点第 3 条)。
### 3.3 我的客户(对应 02 · P2/P3)
| 接口名 | 形式 | 代码位置 | 请求字段 | 响应字段 | 用途 |
| ----------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------- |
| `customer_collect_list` | domain service | [my-customers.vue:139](../src/pages/subpackages/customer/my-customers.vue#L139)[customer-detail.vue:252](../src/pages/subpackages/customer/customer-detail.vue#L252) | `user_combo`(搜索)、`deal_date`(成交时间筛选)、`page``page_size` | `data_list[]`(含 `order_amount`, `order_commission`)、`rc_count` | 合伙人"我的客户"列表 + 客户详情汇总 |
| `customer_order_list` | domain service | [customer-detail.vue:278](../src/pages/subpackages/customer/customer-detail.vue#L278) | `UserMemberId` | `orders[]``order_amount, order_commission, order_fee_amount, product_type`) | 客户详情页的订单明细 |
**⚠️ 本次新需求是否需要改动**
1. 02 原型 P2 逻辑注释要求客户列表能区分归属来源(本人直推成交 / 本人提交商机成单 / 落地页扫码绑定),当前响应字段里没有看到来源标记,建议补充 `source_type` 字段。
2. 02 原型 P3 客户详情要求展示"来源渠道"、"意向产品"、"首次成单"状态、"邀请合伙人"字段,当前 `customer_collect_list` 详情场景返回的字段需要核对是否已覆盖这些。
### 3.4 推荐线索 / 商机(对应 02 · P8/P9,合伙人视角)
| 接口名 | 形式 | 代码位置 | 请求字段 | 用途 |
| ------------------------------------------ | -------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `partner_users` / `create_lead_suggestion` | uniplat model action | [referral-clue.vue:249](../src/pages/subpackages/other/referral-clue.vue#L249) | `{suggestion_type, customer_name, customer_mobile, intention_products, partner_uid, firm_name}` | 提交客户推荐/线索(对应 02 原型 P9"提交新线索") |
| `get_product_list` | domain service | [pop-tabs-select.vue:309](../src/components/common/pop-tabs-select.vue#L309) | `{category_type, load_product}` | 提交线索时选择意向产品的分类树 |
**⚠️ 本次新需求是否需要改动**
1. 02 原型 P8"推荐线索"列表(跟进中/已成单/已作废 tab,含合伙人/对接人/渠道管理员字段)**当前前端没有对应页面代码**,只有提交表单(`referral-clue.vue`)有接口,列表页本身还没实现,需要新增列表查询接口。
2. 02 P9 逻辑注释要求的"去重检测"(同客户+同产品+进行中状态 → 不新建,只刷新更新时间并提示)需要后端在 `create_lead_suggestion` 里实现,或者前端调用前先查询判断。
3. **这里的"商机"和 04 对接人视角的"商机"是同一份数据的两个查看角度**,建议后端只建一套商机表,通过角色权限控制查询范围,具体见「四、当前完全没有的接口」的商机模块说明。
### 3.5 我的佣金(对应 02 · P4)
统一定义在 [commission/request.ts](../src/pages/subpackages/commission/request.ts),均为领域服务,`subProjectName` 主要是 `welfare_bean`/`reward_account_api`
| 接口名 | 请求字段 | 响应字段 | 用途 |
| ----------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `account_info` | — | `balance, will_overdue_amount, money, money_frozen, money_cash, month_tax_free, tax_free, tax_rate, award_money` | 账户余额总览 |
| `account_record_list` | `page_index, page_size, income_type` | `IncomeTypeName, OutcomeValue/IncomeValue, ActivityDescription, CreatedDate` | 收支流水 |
| `apply_tax_amount_info` | `reward_amount, payee_type` | — | 提现前的税额预检 |
| `apply` | `reward_amount, payee_type, payee_identity`(微信通道追加 `wx_app_id, wx_open_id`) | `order_id` | 提交提现申请 |
| `apply_record_list` | `page_index, page_size` | `list[]``cash_type_name, amount, create_time`) | 提现记录列表 |
| `apply_payee_info` | — | — | 收款账户信息 |
| `apply_record_detail` | `id`(apply_id) | — | 提现记录详情 |
| `get_deal_settlement` | `status, type` | `balance, settled_commission, pending_commission, total_commission, list[]``ID, DocNo, ProductName, ServiceTypeName, UserName, CreateDate, CheckDate, Status, StatusName, Amount, Commission, RuleSnapshot`) | 我的佣金余额+明细(02 原型 P4 的主要数据源) |
**⚠️ 本次新需求是否需要改动**`get_deal_settlement` 的结构是"可提现余额"模型(合伙人专用),04 原型的对接人佣金是"次月工资发放,无提现"模型,字段维度也不同(对接人要看"已结算订单数",合伙人模型里没有这个字段)。这两种模型是否共用一张结算表、只是展示口径不同,还是完全独立的两套逻辑,需要和后端确认(详见「四」中的商机/订单模块说明)。
### 3.6 产品(对应 01 首页 + 02 · P5/P6)
| 接口名 | 形式 | 代码位置 | 请求字段 | 响应字段 | 用途 |
| -------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------- |
| `get_category_list` | domain service | [home/components/tools.ts:14](../src/pages/home/components/tools.ts#L14) | `category_type` | 分类树 | 首页/产品列表的分类 tab |
| `partner_product` / `client_main_list` | uniplat model(走 sdkList) | [main-service.vue:140](../src/pages/home/components/main-service.vue#L140) | — | 见下方 `productPredict` | 首页"主推服务"卡片 |
| `partner_product` / `client_detail` | uniplat model | [home/components/index.ts:67](../src/pages/home/components/index.ts#L67) | `id` | 见下方 `productPredict` | 产品详情、海报生成的共用数据源 |
| `get_product_list` | domain service | [product-list.vue:230](../src/pages/subpackages/product/product-list.vue#L230) | `category_type, search_key` | 分类→标签→产品三层树 | 产品列表页(02 原型 P5) |
| `product_content_list` | domain service | [share-view.vue:171](../src/pages/subpackages/product/components/share-view.vue#L171) | `product_id` | 按类型分组(文案/海报/短视频/FAQ),字段 `content, content_type, description` | 产品分享物料 tab(02 原型 P6 推广物料区) |
`productPredict` 完整字段(供参考现有产品模型有哪些字段):
```
category_name, category_category, fee_return_type, fee_rule, fee_type, user_type,
price_str, recommend_flag, product_info_InnerName, product_info_OuterName,
product_info_Status, product_info_ID, manager_name, manager_mobile, description,
fee_amount, fee_per, title, product_code, content, price_desc, fee_desc,
price_min, price_max, intro, object_data, partner_poster_template,
contactor_poster_template, partner_land_page, customer_land_page
```
注意其中已经有 `manager_name` / `manager_mobile`(产品负责人)——04 原型 P10/P11/P12/P13 需要的"产品负责人"字段结构在现有模型里已经存在,本次不需要新增字段,只是 UI 展示方式待定。
**⚠️ 本次新需求是否需要改动**:暂不需要改动此模块接口。
### 3.7 海报(对应 02 · P7)
复用产品模块的 `partner_product`/`client_detail`(拿 `partner_poster_template` / `contactor_poster_template` / `partner_land_page` / `customer_land_page`),加上以下几个二维码相关接口:
| 接口名 | 代码位置 | 用途 |
| -------------------------- | ---------------------------------------------------------- | ------------------------------- |
| `gen_qrcode_code` | [tools.ts:264](../src/pages/home/components/tools.ts#L264) | 生成海报二维码短码 |
| `get_qrcode_info` | [tools.ts:287](../src/pages/home/components/tools.ts#L287) | 扫码还原参数(`product_id` 等) |
| `get_qrcode_image` | [tools.ts:320](../src/pages/home/components/tools.ts#L320) | 生成小程序二维码图 |
| `bind_contactor_for_login` | [tools.ts:360](../src/pages/home/components/tools.ts#L360) | 扫码绑定合伙人/介绍人关系 |
**⚠️ 本次新需求是否需要改动**:暂不需要改动。
---
## 四、当前完全没有的接口
### 4.1 介绍人模块(对应 03 · 全部页面)
前端页面([my-partners.vue](../src/pages/subpackages/customer/my-partners.vue)[partner-detail.vue](../src/pages/subpackages/customer/partner-detail.vue)[introducer-view.vue](../src/pages/home/components/introducer-view.vue))目前**全部是硬编码 mock 数据**,没有调用任何后端接口。建议新增:
#### 【建议新增】介绍人统计概览
- 用途:首页介绍人板块的三宫格(介绍合伙人数 / 推荐企业数 / 待领奖励)
- 参考 mock:[introducer-view.vue:68](../src/pages/home/components/introducer-view.vue#L68)`stat` 对象)
- 建议响应字段:`partner_count, customer_count, pending_amount, pending_count`
#### 【建议新增】我介绍的合伙人列表
- 用途:介绍人业务详情页(03 · P2)
- 参考 mock:[my-partners.vue:132](../src/pages/subpackages/customer/my-partners.vue#L132)
- 建议响应字段:
- 顶部汇总:`partner_count, deal_count, no_deal_count, reward_total, reward_pending, reward_pending_count`
- 列表项:`id, name, mobile, join_date, customer_count, deal_count, reward_status`(待发放/已发放/无奖励)
#### 【建议新增】合伙人详情(介绍人视角)
- 用途:合伙人详情页(03 · P3)
- 参考 mock:[partner-detail.vue:125](../src/pages/subpackages/customer/partner-detail.vue#L125)
- 建议响应字段:
- 头部:`name, mobile, join_date, join_days, customer_count, deal_count`
- 奖励:`reward_amount, reward_status, reward_trigger_customer, reward_date`
- 推荐企业列表:`customer_name, contact_name, contact_mobile, is_deal, invite_date, deal_date, settled_days`
**是否可复用现有结构**:奖励发放规则(¥50/人,首单成交即结算,渠道管理员线下审核发放)和「三、3.5 我的佣金」里的 `get_deal_settlement` 佣金结算逻辑是同一套"审核后发放"模式,建议复用同一套审核/发放机制,只是介绍人奖励是固定金额、合伙人佣金是比例/固定混合,可能是同一张结算表里的不同规则类型。
### 4.2 商机 / 订单模块(对应 04 · 全部页面)
`pages/subpackages/opportunity` 整个分包**目前 100% 是前端 mock**,没有接入任何后端接口(详见 API 清单梳理阶段的确认结论)。这是本次新增最大的一块,建议接口如下:
#### 【建议新增】商机列表
- 参考 mock:[opportunity-list.vue:107](../src/pages/subpackages/opportunity/opportunity-list.vue#L107)
- 建议请求字段:`status`(跟进中/暂时挂起/已成单/已作废)、`product_ids`(我负责的产品,多选)、`source`(商机来源)、`page/page_size`
- 建议响应字段:`customer_name, customer_mobile, product_name, status, partner_name, admin_name, source, create_time, update_time, hold_reason`
- **是否可复用**:与「三、3.4」合伙人视角的商机列表**建议是同一张表**,区别只是查询条件里的角色范围不同(合伙人查"我提交的",对接人查"分配给我的")
#### 【建议新增】商机详情
- 参考 mock:[opportunity-detail.vue:127](../src/pages/subpackages/opportunity/opportunity-detail.vue#L127)
- 建议响应字段:
- 客户信息:`customer_name, contact_name, contact_mobile, product_name, source`
- 关联干系人:介绍人/合伙人/渠道管理员/对接人四方的 `role, name, mobile`
- 状态流转记录:`time, title, desc` 数组
#### 【建议新增】商机挂起 / 作废 / 标记成单(三个动作接口)
- 参考 mock:[hold-popup.vue](../src/pages/subpackages/opportunity/components/hold-popup.vue)[void-popup.vue](../src/pages/subpackages/opportunity/components/void-popup.vue)[opportunity-detail.vue:224](../src/pages/subpackages/opportunity/opportunity-detail.vue#L224)`markDone`
- 挂起:`{opportunity_id, reason, remark}`,reason 为单选(客户预算暂未确认/决策周期较长/等客户内部审批/竞品比价中/其他)
- 作废:`{opportunity_id, reason, remark}`,reason 单选(客户已选择竞品/客户失联无意向/预算不足/客户资质不符/重复商机/无法提供对应服务/其他),选"其他"时 remark 必填
- 标记成单:`{opportunity_id}` → 生成对应订单记录(需要后端定义"商机成单"和"订单创建"的事务关系)
#### 【建议新增】新增商机(对接人代录入)
- 参考 mock:[opportunity-create.vue:118](../src/pages/subpackages/opportunity/opportunity-create.vue#L118)
- 建议请求字段:`customer_type, customer_name, contact_name, contact_mobile, product_names[], remark, partner_id?`
- **需要后端实现去重检测**:同客户+同产品已有进行中商机 → 不新建,只提示并刷新原商机更新时间(04 原型逻辑注释明确要求)
- 来源固定标记为"后台添加",区别于合伙人小程序提交的"合伙人添加"
#### 【建议新增】订单列表 / 订单详情
- 参考 mock:[order-list.vue:80](../src/pages/subpackages/opportunity/order-list.vue#L80)[order-detail.vue:113](../src/pages/subpackages/opportunity/order-detail.vue#L113)
- 列表建议字段:`customer_name, product_name, order_no, amount, rate, commission, settled`
- 详情额外字段:`order_time`,以及分佣干系人列表(对接人本人显示实际金额,其他三方只显示分佣规则文案,不显示金额)
- **是否可复用**:与「三、3.5」的 `get_deal_settlement` 明细字段高度相似(`Amount, Commission, RuleSnapshot` 等),建议复用同一张订单/结算表,只是对接人视角不需要"提现"操作,是"次月随工资发放"
#### 【建议新增】我负责的产品列表 / 详情
- 参考 mock:[my-product-list.vue:74](../src/pages/subpackages/product/my-product-list.vue#L74)[my-product-detail.vue:107](../src/pages/subpackages/product/my-product-detail.vue#L107)
- 列表建议字段:`name, category, price_desc, rate_desc, hot, following_count, done_count, month_commission`
- 详情额外字段:`desc, rate, rate_base, total_commission`,以及跟进中商机预览列表
- **是否可复用**:产品基础信息复用「三、3.6」的 `partner_product` 模型(已有 `manager_name`/`manager_mobile` 负责人字段),只是这里需要额外按"主责人=当前对接人"过滤,并联查商机/订单表算出统计数字,不是一张新的产品表,是产品表 + 商机表的聚合查询
---
## 五、待确认疑点清单
1. **"介绍人"身份判定条件在不同原型文件里不一致**
- 01 原型(P3a→P3b):只要"曾推荐过至少 1 位合伙人(无论是否成单)"即可
- 02 原型(P1a→P1b)、03 原型:需要"下级合伙人已有企业客户产生订单"才算
- 需要和产品/后端确认哪个是最终口径,这直接决定 `isIntroducer` 后端判定逻辑。
2. **`isIntroducer` 当前是前端占位实现**[services/type.ts:7](../src/services/type.ts#L7)),依赖 `userInfo2.is_introducer` 字段,需要确认后端 `user_info` 接口是否已返回或计划何时返回该字段,以及是实时计算字段还是落库字段。
3. **01 原型 P4 申请表单的"身份证号"必填性矛盾**:表单 UI 上没有标红色必填星号,但逻辑注释文字里把它列为必填校验项,需要产品确认。同时"手机号验证码"和"介绍人"两个输入控件在原型 HTML 里是空容器,没有具体 placeholder 文案,需要补充设计稿或确认字段细节。
4. **"对接人分配表"具体结构未知**:02、04 两份原型反复提到新增商机时要查这张表来自动匹配对接人/渠道管理员,但现有前端代码和数据库层面完全看不到这张表的痕迹,需要和后端确认是否已存在、由谁维护、维护入口在哪个后台。
5. **04 对接人版"我的佣金"页面的显示分支条件尚未最终确定**:讨论中曾出现"对接人显示新卡片"、"非合伙人非对接人显示新卡片"两种相反的说法,最终标记为"存疑,后续再定",目前 [my-commission.vue](../src/pages/subpackages/commission/my-commission.vue) 未做改动(原文件已备份为 `my-commission.bak.vue`)。这部分接口需求要等业务分支定案后才能确定。
6. **04 P3"商机筛选"目前实现成独立页面**,产品方表示"UI 稿未出,后续可能改成弹窗",如果改成弹窗,筛选参数的传递方式不受影响(都是查询参数),但如果后端有埋点/统计需求区分"进入筛选页"这个动作,需要提前告知。
7. **商机"逾期自动作废"规则**(04 原型:跟进中超期提醒、挂起状态"逾期 30 天自动作废")是纯后端定时任务规则,前端不参与,仅供后端实现时参考,具体阈值天数需要产品最终确认(原型里的"30 天"是示例数字)。
8. **04 原型 P8"分佣干系人"展示口径**:对接人本人的佣金显示实际到手金额,其他三方(介绍人/合伙人/渠道管理员)只显示各自的分佣规则文案、不显示具体金额——这是一个权限/展示层面的要求,需要接口按角色返回不同粒度的数据,而不是前端拿到全部数据后自行隐藏(避免金额数据被绕过接口拿到)。
++ "a/\344\272\262\344\272\262\345\210\233\345\256\242\344\272\214\346\234\237/\346\226\207\346\241\243/\350\201\224\350\260\203\350\256\260\345\275\225.md"
No preview for this file type
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<meta name="design_doc_mode" content="canvas">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Noto Sans SC', -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; -webkit-font-smoothing: antialiased; }
.phone { width:375px; height:812px; background:linear-gradient(180deg,#E8EAFA 0%,#EEE9F6 50%,#F4F1FA 100%); border-radius:42px; overflow:hidden; box-shadow:0 24px 50px rgba(60,55,120,.18),0 4px 14px rgba(0,0,0,.08); position:relative; display:flex; flex-direction:column; }
.statusbar { height:44px; display:flex; align-items:center; padding:0 28px 0 32px; flex-shrink:0; z-index:3; }
.stime { font:600 16px/1 -apple-system,sans-serif; flex:1; color:#1A1A2E; }
.sicons { display:flex; gap:5px; align-items:center; color:#1A1A2E; }
.navbar { height:44px; display:flex; align-items:center; padding:0 6px 0 16px; flex-shrink:0; z-index:2; }
.navtitle { flex:1; text-align:center; font:600 17px/1 'Noto Sans SC',sans-serif; color:#1A1A2E; padding-right:88px; padding-left:32px; }
.capsule { width:87px; height:32px; background:rgba(255,255,255,.7); border-radius:16px; border:.5px solid rgba(0,0,0,.08); display:flex; align-items:center; justify-content:space-around; flex-shrink:0; }
.scroll { flex:1; overflow-y:auto; }
.scroll::-webkit-scrollbar { width:3px; }
.scroll::-webkit-scrollbar-thumb { background:rgba(0,0,0,.1); border-radius:2px; }
.home { height:34px; display:flex; align-items:flex-end; justify-content:center; padding-bottom:8px; flex-shrink:0; }
.home-bar { width:134px; height:5px; background:#1A1A2E; border-radius:3px; }
.back-ic { width:28px; height:28px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.label { position:absolute; top:-26px; font:500 12px/1 -apple-system,sans-serif; color:rgba(40,30,80,.55); letter-spacing:.3px; }
.input { height:48px; background:#fff; border-radius:12px; border:1px solid #EAEAF2; display:flex; align-items:center; padding:0 16px; font-size:14px; color:#BFBFC7; }
.input.filled { color:#1A1A2E; }
.btn-primary { height:50px; background:linear-gradient(135deg,#4D6FF5,#3651DB); border-radius:25px; display:flex; align-items:center; justify-content:center; color:#fff; font-size:16px; font-weight:600; letter-spacing:1px; box-shadow:0 8px 20px rgba(77,111,245,.32); }
.tag-qfu { background:#E8EEFE; color:#4A6CF7; font-size:11px; padding:2px 7px; border-radius:4px; font-weight:500; }
.tag-qbao { background:#E5F7EC; color:#16A55A; font-size:11px; padding:2px 7px; border-radius:4px; font-weight:500; }
.section-bar { width:3px; height:14px; background:#4A6CF7; border-radius:2px; }
.role-chip { padding:9px 14px; background:#F4F4F8; color:#666; font-size:13px; border-radius:10px; border:1.5px solid transparent; font-weight:500; text-align:center; }
.role-chip.active { background:linear-gradient(135deg,#EAF0FE,#DDE6FF); color:#3651DB; border-color:#4D6FF5; font-weight:600; }
</style>
</helmet>
<!-- ===== CANVAS HEADER ===== -->
<div style="position:absolute;left:80px;top:14px;font:800 22px/1 'Noto Sans SC',sans-serif;color:#1A1A2E;letter-spacing:-.4px">登录及申请</div>
<div style="position:absolute;left:80px;top:46px;display:flex;align-items:center;gap:10px;font:500 12px/1 'Noto Sans SC',sans-serif;color:rgba(40,30,80,.6)">
<span style="display:inline-block;width:18px;height:2px;background:#4D6FF5"></span>
<span>未登录首页 · 登录页 · 非介绍人状态 · 介绍人状态 · 申请成为合伙人</span>
</div>
<!-- ================================================================
P1 · 未登录首页(与合伙人首页一致,仅登录状态区分 + 登录引导)
================================================================ -->
<div style="position:absolute;left:80px;top:120px;width:375px">
<div data-drags-parent="1" class="label">P1 · 未登录首页</div>
<div class="phone">
<div class="statusbar">
<span class="stime">9:41</span>
<div class="sicons"><svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><circle cx="2" cy="9.5" r="1.5"></circle><circle cx="6.5" cy="9.5" r="1.5"></circle><circle cx="11" cy="9.5" r="1.5"></circle></svg><svg width="16" height="11" viewBox="0 0 16 11" fill="none"><circle cx="8" cy="9.5" r="1" fill="currentColor"></circle></svg><svg width="26" height="13" viewBox="0 0 26 13" fill="none"><rect x=".5" y="1.5" width="22" height="10" rx="2.5" stroke="currentColor" stroke-opacity=".4"></rect><rect x="2" y="3" width="19" height="7" rx="1.2" fill="currentColor"></rect></svg></div>
</div>
<div class="navbar"><div style="flex:1"></div><div class="capsule"><svg width="18" height="4" viewBox="0 0 18 4"><circle cx="3" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="9" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="15" cy="2" r="1.5" fill="#1A1A2E"></circle></svg><div style="width:.5px;height:14px;background:rgba(0,0,0,.15)"></div><svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" stroke="#1A1A2E" stroke-width="1.1" fill="none"></circle><circle cx="8" cy="8" r="2.2" fill="#1A1A2E"></circle></svg></div></div>
<div class="scroll">
<!-- Profile placeholder · 未登录态 -->
<!-- 业务介绍 Hero -->
<div style="margin:0 16px 0;background:linear-gradient(135deg,#5B7CFA 0%,#3651DB 60%,#7B5FE8 100%);border-radius:18px;padding:20px;color:#fff;position:relative;overflow:hidden">
<div style="position:absolute;right:-30px;top:-30px;width:140px;height:140px;border-radius:50%;background:rgba(255,255,255,.1)"></div>
<div style="position:absolute;right:14px;bottom:-30px;width:80px;height:80px;border-radius:50%;background:rgba(255,255,255,.08)"></div>
<div style="position:relative">
<div style="display:inline-flex;align-items:center;gap:5px;background:rgba(255,255,255,.22);border-radius:10px;padding:3px 9px;font-size:11px;margin-bottom:10px">🚀 推荐好服务 · 共享好收益</div>
<div style="font-size:22px;font-weight:800;line-height:1.25;margin-bottom:8px">亲亲创客<br>一站式企业服务推广平台</div>
<div style="font-size:12.5px;color:rgba(255,255,255,.88);line-height:1.65;margin-bottom:14px">推荐企业开公司、办执照、人事社保<br>订单成交即享高额佣金 · 续费持续返佣</div>
<div style="display:flex;gap:8px">
<div style="flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;background:#fff;color:#3651DB;font-size:13.5px;font-weight:700;padding:10px 0;border-radius:18px;box-shadow:0 6px 14px rgba(0,0,0,.12)">一键登录 ›</div>
<div style="flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.45);color:#fff;font-size:13.5px;font-weight:600;padding:10px 0;border-radius:18px">申请入驻 ›</div>
</div>
</div>
</div>
<!-- 价值点 -->
<div style="margin:12px 16px 0;display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
<div style="background:#fff;border-radius:12px;padding:12px 8px;text-align:center;box-shadow:0 2px 8px rgba(60,55,120,.05)">
<div style="font-size:22px;line-height:1;margin-bottom:6px">🏢</div>
<div style="font-size:12px;font-weight:600;color:#1A1A2E">企业服务严选</div>
<div style="font-size:10px;color:#888;margin-top:3px">官方合规品类</div>
</div>
<div style="background:#fff;border-radius:12px;padding:12px 8px;text-align:center;box-shadow:0 2px 8px rgba(60,55,120,.05)">
<div style="font-size:22px;line-height:1;margin-bottom:6px">💰</div>
<div style="font-size:12px;font-weight:600;color:#1A1A2E">最高 30% 返佣</div>
<div style="font-size:10px;color:#888;margin-top:3px">续费持续分润</div>
</div>
<div style="background:#fff;border-radius:12px;padding:12px 8px;text-align:center;box-shadow:0 2px 8px rgba(60,55,120,.05)">
<div style="font-size:22px;line-height:1;margin-bottom:6px"></div>
<div style="font-size:12px;font-weight:600;color:#1A1A2E">小程序秒提现</div>
<div style="font-size:10px;color:#888;margin-top:3px">结算公开透明</div>
</div>
</div>
<!-- Main services (locked overlay) -->
<div style="margin:14px 16px 0;background:linear-gradient(180deg,#FFF1E2,#FFFCF6);border-radius:16px;padding:14px;position:relative">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:6px"><svg width="16" height="16" viewBox="0 0 16 16" fill="#FFD86B" stroke="#E89B14" stroke-width="1.3" stroke-linejoin="round"><path d="M8 1l1.8 4 4.2.4-3.2 2.8 1 4.2L8 10.4l-3.8 2 1-4.2L1 5.4l4.2-.4L8 1z"></path></svg><span style="font-size:15px;font-weight:800;font-style:italic;color:#A8430F;letter-spacing:.5px">主推服务</span></div>
<span style="font-size:11px;color:#A8430F;opacity:.7">公司严选好品 · 出单更稳更省心</span>
</div>
<div style="background:#fff;border-radius:12px;padding:14px;margin-bottom:10px;display:flex;align-items:center;gap:12px">
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px"><span style="background:linear-gradient(135deg,#FF8A4E,#FF6B35);color:#fff;font-size:10px;padding:2px 7px;border-radius:5px;font-weight:600">🔥 高佣金</span><span style="font-size:15px;font-weight:700;color:#1A1A2E">个体工商户/公司注册</span></div>
<div style="font-size:12px;color:#888;margin-bottom:8px">开公司办执照,全程合规代办</div>
<div style="display:flex;gap:6px"><span class="tag-qbao">亲亲小保</span><span class="tag-qfu">个人创业支持服务</span></div>
</div>
<div style="text-align:center"><div style="font-size:24px;font-weight:800;color:#FF6B35;line-height:1">30%</div><div style="font-size:10px;color:#FF6B35;margin-top:2px">佣金服务费的</div><div style="margin-top:8px;background:linear-gradient(135deg,#FFB36A,#FF6B35);color:#fff;font-size:12px;font-weight:600;padding:5px 16px;border-radius:14px">查看</div></div>
</div>
<div style="background:#fff;border-radius:12px;padding:14px;display:flex;align-items:center;gap:12px">
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px"><span style="background: #EAF0FE; color: #4A6CF7; border: 1px solid #BFD0FA; font-size: 10px; padding: 2px 7px; border-radius: 5px; font-weight: 600; width: 70px">👍 易成交</span><span style="font-size:15px;font-weight:700;color:#1A1A2E">虚拟注册地/租赁注册地址</span></div>
<div style="font-size:12px;color:#888;margin-bottom:8px">全流程代办零操心</div>
<div style="display:flex;gap:6px"><span class="tag-qfu">亲亲企服</span><span class="tag-qfu">人事用工服务</span></div>
</div>
<div style="text-align:center"><div style="font-size:24px;font-weight:800;color:#FF6B35;line-height:1">10%</div><div style="font-size:10px;color:#FF6B35;margin-top:2px">佣金服务费的</div><div style="margin-top:8px;background:linear-gradient(135deg,#FFB36A,#FF6B35);color:#fff;font-size:12px;font-weight:600;padding:5px 16px;border-radius:14px">查看</div></div>
</div>
<div style="text-align:center;margin-top:10px"><span style="font-size:12px;color:#A8430F;font-weight:500">更多服务 ⌄</span></div>
</div>
<!-- Service grids -->
<div style="margin:14px 16px 0">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="display:flex;align-items:center;gap:7px"><div style="width:24px;height:24px;background:linear-gradient(135deg,#7B9AFF,#4A6CF7);border-radius:6px;display:flex;align-items:center;justify-content:center"><span style="color:#fff;font-size:13px;font-weight:700"></span></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">亲亲企服</span></div>
<span style="font-size:12px;color:#888">更多服务 ›</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">人事用工服务</div><div style="font-size:11px;color:#888">零基础可推</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">工商财税服务</div><div style="font-size:11px;color:#888">官方严选</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">企业出海服务</div><div style="font-size:11px;color:#4A6CF7">全新上线</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">数字员工服务</div><div style="font-size:11px;color:#16A55A">新品首发</div></div>
</div>
</div>
<div style="margin:14px 16px 0;padding-bottom:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="display:flex;align-items:center;gap:7px"><div style="width:24px;height:24px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:6px;display:flex;align-items:center;justify-content:center"><span style="color:#fff;font-size:13px;font-weight:700"></span></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">亲亲小保</span></div>
<span style="font-size:12px;color:#888">更多服务 ›</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">Offer分析/择业规划</div><div style="font-size:11px;color:#888">零基础可推</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">个体工商户/公司注册</div><div style="font-size:11px;color:#888">官方严选</div></div>
</div>
</div>
</div>
<!-- Tab bar with sticky CTA hint -->
<div class="home"><div class="home-bar"></div></div>
</div>
</div>
<!-- ================================================================
P2 · 登录页
================================================================ -->
<div style="position:absolute;left:555px;top:120px;width:375px">
<div data-drags-parent="1" class="label">P2 · 登录页</div>
<div class="phone">
<div class="statusbar">
<span class="stime">9:41</span>
<div class="sicons"><svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><circle cx="2" cy="9.5" r="1.5"></circle><circle cx="6.5" cy="9.5" r="1.5"></circle><circle cx="11" cy="9.5" r="1.5"></circle></svg><svg width="16" height="11" viewBox="0 0 16 11" fill="none"><circle cx="8" cy="9.5" r="1" fill="currentColor"></circle></svg><svg width="26" height="13" viewBox="0 0 26 13" fill="none"><rect x=".5" y="1.5" width="22" height="10" rx="2.5" stroke="currentColor" stroke-opacity=".4"></rect><rect x="2" y="3" width="19" height="7" rx="1.2" fill="currentColor"></rect></svg></div>
</div>
<div style="position:absolute;right:14px;top:48px;z-index:3"><div class="capsule"><svg width="18" height="4" viewBox="0 0 18 4"><circle cx="3" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="9" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="15" cy="2" r="1.5" fill="#1A1A2E"></circle></svg><div style="width:.5px;height:14px;background:rgba(0,0,0,.15)"></div><svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" stroke="#1A1A2E" stroke-width="1.1" fill="none"></circle><circle cx="8" cy="8" r="2.2" fill="#1A1A2E"></circle></svg></div></div>
<div class="scroll" style="display:flex;flex-direction:column;align-items:center;padding:0 28px">
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%">
<div style="position:relative;width:180px;height:180px;margin-bottom:14px;display:flex;align-items:center;justify-content:center">
<div style="position:absolute;inset:0;border-radius:50%;border:1px solid rgba(91,124,250,.12)"></div>
<div style="position:absolute;inset:18px;border-radius:50%;border:1px solid rgba(91,124,250,.18)"></div>
<div style="position:absolute;inset:36px;border-radius:50%;border:1px solid rgba(91,124,250,.25)"></div>
<div style="width:96px;height:96px;background:linear-gradient(135deg,#5B7CFA,#3651DB);border-radius:24px;display:flex;align-items:center;justify-content:center;box-shadow:0 18px 36px rgba(77,111,245,.4)"><span style="color:#fff;font-size:46px;font-weight:800"></span></div>
</div>
<div style="font:800 30px/1 'Noto Sans SC',sans-serif;color:#3D5DE8;letter-spacing:3px;margin-bottom:12px">亲亲创客</div>
<div style="font-size:15px;color:#666;letter-spacing:1px;margin-bottom:18px">推荐好服务 共享好收益</div>
<!-- 业务描述 -->
<div style="width:100%;background:rgba(255,255,255,.65);border-radius:14px;padding:14px 16px;backdrop-filter:blur(8px);box-shadow:0 4px 14px rgba(60,55,120,.06);border:1px solid rgba(255,255,255,.8)">
<div style="font-size:13px;color:#1A1A2E;line-height:1.7;text-align:center"><b style="color: rgb(54, 81, 219);">一站式企业服务推广平台</b></div>
<div style="margin-top:10px;display:flex;align-items:center;justify-content:center;gap:10px;font-size:11px;color:#666">
<span>👥 推荐企业</span><span style="opacity:.35">·</span><span>💰 订单返佣</span><span style="opacity:.35">·</span><span>⚡ 秒到账</span>
</div>
</div>
</div>
<div style="width:100%;padding-bottom:8px">
<div style="display:flex;align-items:flex-start;gap:8px;font-size:12px;color:#666;line-height:1.6;margin-bottom:16px;padding:0 4px">
<div style="width:16px;height:16px;border-radius:50%;border:1.5px solid #C8C5DE;flex-shrink:0;margin-top:1px"></div>
<span>我已阅读并同意<span style="color:#1A1A2E">《亲亲创客用户使用协议》</span><span style="color:#1A1A2E">《用户隐私政策》</span></span>
</div>
<div style="height:50px;background:linear-gradient(135deg,#5B7CFA,#3651DB);border-radius:25px;display:flex;align-items:center;justify-content:center;gap:9px;box-shadow:0 8px 22px rgba(77,111,245,.35);margin-bottom:12px">
<svg width="22" height="22" viewBox="0 0 24 24" fill="#fff"><path d="M9.5 4C5.36 4 2 6.69 2 10c0 1.89 1.1 3.57 2.82 4.66l-.7 2.1 2.43-1.3c.62.17 1.27.27 1.95.27.2 0 .4-.01.6-.03-.13-.4-.2-.83-.2-1.27 0-2.66 2.6-4.83 5.8-4.83.18 0 .36.01.54.02C14.8 6.36 12.4 4 9.5 4z"></path><path d="M22 14.83c0-2.21-2.13-4-4.75-4S12.5 12.62 12.5 14.83c0 2.22 2.13 4 4.75 4 .5 0 .98-.07 1.43-.18l1.82.97-.5-1.57c.93-.78 1.5-1.83 1.5-3.02z"></path></svg>
<span style="color:#fff;font-size:16px;font-weight:600">手机号一键登录</span>
</div>
<div style="height:50px;background:#fff;border-radius:25px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 14px rgba(60,55,120,.08);margin-bottom:24px"><span style="color:#1A1A2E;font-size:15px;font-weight:500">其他登录方式</span></div>
<div style="text-align:center;padding-bottom:6px"><span style="font-size:13px;color:#999;background:rgba(255,255,255,.6);padding:6px 18px;border-radius:14px">暂不登录</span></div>
</div>
</div>
<div class="home"><div class="home-bar"></div></div>
</div>
</div>
<!-- ================================================================
P3 · 已登录非合伙人首页(引导申请合伙人)
================================================================ -->
<div style="position:absolute;left:1030px;top:120px;width:375px">
<div data-drags-parent="1" class="label">P3a · 非介绍人状态</div>
<div class="phone">
<div class="statusbar">
<span class="stime">9:41</span>
<div class="sicons"><svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><circle cx="2" cy="9.5" r="1.5"></circle><circle cx="6.5" cy="9.5" r="1.5"></circle><circle cx="11" cy="9.5" r="1.5"></circle></svg><svg width="16" height="11" viewBox="0 0 16 11" fill="none"><circle cx="8" cy="9.5" r="1" fill="currentColor"></circle></svg><svg width="26" height="13" viewBox="0 0 26 13" fill="none"><rect x=".5" y="1.5" width="22" height="10" rx="2.5" stroke="currentColor" stroke-opacity=".4"></rect><rect x="2" y="3" width="19" height="7" rx="1.2" fill="currentColor"></rect></svg></div>
</div>
<div class="navbar"><div style="flex:1"></div><div class="capsule"><svg width="18" height="4" viewBox="0 0 18 4"><circle cx="3" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="9" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="15" cy="2" r="1.5" fill="#1A1A2E"></circle></svg><div style="width:.5px;height:14px;background:rgba(0,0,0,.15)"></div><svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" stroke="#1A1A2E" stroke-width="1.1" fill="none"></circle><circle cx="8" cy="8" r="2.2" fill="#1A1A2E"></circle></svg></div></div>
<div class="scroll">
<!-- Profile · 已登录,非合伙人 -->
<div style="padding:6px 18px 14px;display:flex;align-items:center;gap:12px">
<div style="width:54px;height:54px;background:linear-gradient(135deg,#B5B3CD,#7B7AA0);border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:2px solid #fff;box-shadow:0 4px 12px rgba(60,55,120,.18)">
<span style="color:#fff;font-size:22px;font-weight:700"></span>
</div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px"><span style="font-size:18px;font-weight:700;color:#1A1A2E">杨恒宇</span><span style="background:#F4F4F8;color:#888;font-size:11px;padding:2px 9px;border-radius:10px;font-weight:500;border:1px solid #E4E1F0">普通用户</span></div>
<div style="font-size:13px;color:#666">159****1234&nbsp;</div>
</div>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M6 3l6 6-6 6" stroke="#999" stroke-width="1.6" stroke-linecap="round"></path></svg>
</div>
<!-- HERO · 申请合伙人引导 -->
<div style="margin:0 16px 14px;background:linear-gradient(135deg,#5B7CFA 0%,#3651DB 60%,#7B5FE8 100%);border-radius:18px;padding:18px;color:#fff;position:relative;overflow:hidden">
<div style="position:absolute;right:-30px;top:-30px;width:130px;height:130px;border-radius:50%;background:rgba(255,255,255,.1)"></div>
<div style="position:absolute;right:14px;bottom:-30px;width:80px;height:80px;border-radius:50%;background:rgba(255,255,255,.08)"></div>
<div style="position:relative">
<div style="display:inline-flex;align-items:center;gap:5px;background:rgba(255,255,255,.22);border-radius:10px;padding:3px 9px;font-size:11px;margin-bottom:9px">🎉 你还不是合伙人</div>
<div style="font-size:22px;font-weight:800;line-height:1.2;margin-bottom:6px">申请成为合伙人<br>最高享 <span style="color:#FFD86B">30%</span> 持续返佣</div>
<div style="font-size:12.5px;color:rgba(255,255,255,.85);line-height:1.6;margin-bottom:14px">推荐企业客户 · 订单成交即得佣金 · 提现秒到账</div>
<div style="display:inline-flex;align-items:center;gap:6px;background:#fff;color:#3651DB;font-size:14px;font-weight:700;padding:9px 22px;border-radius:18px;box-shadow:0 6px 14px rgba(0,0,0,.15)">立即申请 ›</div>
</div>
</div>
<!-- 分享邀请别人成为合伙人 -->
<div style="margin:0 16px;background:#fff;border-radius:14px;padding:14px 16px;display:flex;align-items:center;gap:12px;box-shadow:0 4px 14px rgba(60,55,120,.06);border:1px solid rgba(108,211,164,.3)">
<div style="width:42px;height:42px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 4px 10px rgba(22,165,90,.28)">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M8 14a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6-2a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM2 20c0-3.3 2.7-6 6-6s6 2.7 6 6m1.5-5.5c2.5 0 4.5 2 4.5 4.5" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px"><span style="font-size:14px;font-weight:700;color:#1A1A2E">邀请朋友成为合伙人</span><span style="background:#FFEEE0;color:#FF7A2E;font-size:10px;padding:1px 6px;border-radius:4px;font-weight:600">奖¥50</span></div>
<div style="font-size:11.5px;color:#888;line-height:1.5">分享专属海报 / 链接 · 朋友首单成交您即得福礼金</div>
</div>
<div style="height:30px;padding:0 12px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:15px;display:flex;align-items:center;color:#fff;font-size:12px;font-weight:600;flex-shrink:0">去邀请</div>
</div>
<!-- 主推服务 -->
<div style="margin:14px 16px 0;background:linear-gradient(180deg,#FFF1E2,#FFFCF6);border-radius:16px;padding:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:6px"><svg width="16" height="16" viewBox="0 0 16 16" fill="#FFD86B" stroke="#E89B14" stroke-width="1.3" stroke-linejoin="round"><path d="M8 1l1.8 4 4.2.4-3.2 2.8 1 4.2L8 10.4l-3.8 2 1-4.2L1 5.4l4.2-.4L8 1z"></path></svg><span style="font-size:15px;font-weight:800;font-style:italic;color:#A8430F">主推服务</span></div>
<span style="font-size:11px;color:#A8430F;opacity:.7">公司严选 · 出单更稳</span>
</div>
<div style="background:#fff;border-radius:12px;padding:14px;margin-bottom:10px;display:flex;align-items:center;gap:12px">
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px"><span style="background:linear-gradient(135deg,#FF8A4E,#FF6B35);color:#fff;font-size:10px;padding:2px 7px;border-radius:5px;font-weight:600">🔥 高佣金</span><span style="font-size:15px;font-weight:700;color:#1A1A2E">个体工商户/公司注册</span></div>
<div style="font-size:12px;color:#888;margin-bottom:8px">开公司办执照,全程合规代办</div>
<div style="display:flex;gap:6px"><span class="tag-qbao">亲亲小保</span><span class="tag-qfu">个人创业支持服务</span></div>
</div>
<div style="text-align:center"><div style="font-size:24px;font-weight:800;color:#FF6B35;line-height:1">30%</div><div style="font-size:10px;color:#FF6B35;margin-top:2px">佣金服务费的</div><div style="margin-top:8px;background:linear-gradient(135deg,#FFB36A,#FF6B35);color:#fff;font-size:12px;font-weight:600;padding:5px 16px;border-radius:14px">查看</div></div>
</div>
<div style="text-align:center"><span style="font-size:12px;color:#A8430F;font-weight:500">更多服务 ⌄</span></div>
</div>
<!-- Service grids -->
<div style="margin:14px 16px 0">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px"><div style="display:flex;align-items:center;gap:7px"><div style="width:24px;height:24px;background:linear-gradient(135deg,#7B9AFF,#4A6CF7);border-radius:6px;display:flex;align-items:center;justify-content:center"><span style="color:#fff;font-size:13px;font-weight:700"></span></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">亲亲企服</span></div><span style="font-size:12px;color:#888">更多 ›</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">人事用工服务</div><div style="font-size:11px;color:#888">零基础可推</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">工商财税服务</div><div style="font-size:11px;color:#888">官方严选</div></div>
</div>
</div>
<div style="margin:14px 16px 0;padding-bottom:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px"><div style="display:flex;align-items:center;gap:7px"><div style="width:24px;height:24px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:6px;display:flex;align-items:center;justify-content:center"><span style="color:#fff;font-size:13px;font-weight:700"></span></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">亲亲小保</span></div><span style="font-size:12px;color:#888">更多 ›</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">Offer分析/择业规划</div><div style="font-size:11px;color:#888">零基础可推</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">人社保/公积金/个税…</div><div style="font-size:11px;color:#FF6B35">高佣金</div></div>
</div>
</div>
</div>
<div class="home"><div class="home-bar"></div></div>
</div>
</div>
<!-- ================================================================
P3b · 介绍人状态
================================================================ -->
<div style="position:absolute;left:1505px;top:120px;width:375px">
<div data-drags-parent="1" class="label">P3b · 介绍人状态</div>
<div class="phone">
<div class="statusbar">
<span class="stime">9:41</span>
<div class="sicons"><svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><circle cx="2" cy="9.5" r="1.5"></circle><circle cx="6.5" cy="9.5" r="1.5"></circle><circle cx="11" cy="9.5" r="1.5"></circle></svg><svg width="16" height="11" viewBox="0 0 16 11" fill="none"><circle cx="8" cy="9.5" r="1" fill="currentColor"></circle></svg><svg width="26" height="13" viewBox="0 0 26 13" fill="none"><rect x=".5" y="1.5" width="22" height="10" rx="2.5" stroke="currentColor" stroke-opacity=".4"></rect><rect x="2" y="3" width="19" height="7" rx="1.2" fill="currentColor"></rect></svg></div>
</div>
<div class="navbar"><div style="flex:1"></div><div class="capsule"><svg width="18" height="4" viewBox="0 0 18 4"><circle cx="3" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="9" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="15" cy="2" r="1.5" fill="#1A1A2E"></circle></svg><div style="width:.5px;height:14px;background:rgba(0,0,0,.15)"></div><svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" stroke="#1A1A2E" stroke-width="1.1" fill="none"></circle><circle cx="8" cy="8" r="2.2" fill="#1A1A2E"></circle></svg></div></div>
<div class="scroll">
<!-- Profile · 介绍人 -->
<div style="padding:6px 18px 14px;display:flex;align-items:center;gap:12px">
<div style="width:54px;height:54px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:2px solid #fff;box-shadow:0 4px 12px rgba(22,165,90,.28)">
<span style="color:#fff;font-size:22px;font-weight:700"></span>
</div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px"><span style="font-size:18px;font-weight:700;color:#1A1A2E">杨恒宇</span><span style="background:linear-gradient(135deg,#6CD3A4,#16A55A);color:#fff;font-size:11px;padding:2px 9px;border-radius:10px;font-weight:600">介绍人</span></div>
<div style="font-size:13px;color:#666">159****1234&nbsp;</div>
</div>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M6 3l6 6-6 6" stroke="#999" stroke-width="1.6" stroke-linecap="round"></path></svg>
</div>
<!-- HERO · 申请合伙人 -->
<div style="margin:0 16px 14px;background:linear-gradient(135deg,#5B7CFA 0%,#3651DB 60%,#7B5FE8 100%);border-radius:18px;padding:18px;color:#fff;position:relative;overflow:hidden">
<div style="position:absolute;right:-30px;top:-30px;width:130px;height:130px;border-radius:50%;background:rgba(255,255,255,.1)"></div>
<div style="position:absolute;right:14px;bottom:-30px;width:80px;height:80px;border-radius:50%;background:rgba(255,255,255,.08)"></div>
<div style="position:relative">
<div style="display:inline-flex;align-items:center;gap:5px;background:rgba(255,255,255,.22);border-radius:10px;padding:3px 9px;font-size:11px;margin-bottom:9px">🎉 你还不是合伙人</div>
<div style="font-size:22px;font-weight:800;line-height:1.2;margin-bottom:6px">申请成为合伙人<br>最高享 <span style="color:#FFD86B">30%</span> 持续返佣</div>
<div style="font-size:12.5px;color:rgba(255,255,255,.85);line-height:1.6;margin-bottom:14px">推荐企业客户 · 订单成交即得佣金 · 提现秒到账</div>
<div style="display:inline-flex;align-items:center;gap:6px;background:#fff;color:#3651DB;font-size:14px;font-weight:700;padding:9px 22px;border-radius:18px;box-shadow:0 6px 14px rgba(0,0,0,.15)">立即申请 ›</div>
</div>
</div>
<!-- 介绍人业务 板块 -->
<div style="margin:0 16px 0;background:linear-gradient(135deg,#E5F7EC 0%,#D1F0DC 50%,#C7EFE2 100%);border-radius:18px;padding:16px;position:relative;overflow:hidden;border:1.5px solid rgba(22,165,90,.18);box-shadow:0 8px 24px rgba(22,165,90,.12)">
<div style="position:absolute;right:-20px;top:-20px;width:100px;height:100px;border-radius:50%;background:radial-gradient(circle,rgba(108,211,164,.4),transparent 70%)"></div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;position:relative">
<div style="width:38px;height:38px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:11px;display:flex;align-items:center;justify-content:center;box-shadow:0 6px 14px rgba(22,165,90,.32)">
<svg width="20" height="20" viewBox="0 0 22 22" fill="none"><path d="M8 14a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6-2a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM2 20c0-3.3 2.7-6 6-6s6 2.7 6 6m1.5-5.5c2.5 0 4.5 2 4.5 4.5" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</div>
<div style="flex:1">
<div style="font-size:16px;font-weight:800;color:#0E5A30;letter-spacing:-.2px">邀请合伙人</div>
<div style="font-size: 11px; color: #1A8C4D; margin-top: 2px">推荐他人成为合伙人 · 首单成交奖¥50</div>
</div>
<span style="background:#FF8145;color:#fff;font-size:10px;padding:3px 8px;border-radius:9px;font-weight:600">2 笔待领</span>
</div>
<div style="background:rgba(255,255,255,.78);border-radius:12px;padding:12px 0;display:flex;backdrop-filter:blur(8px);position:relative">
<div style="flex:1;text-align:center"><div style="font-size:22px;font-weight:800;color:#16A55A;line-height:1">3</div><div style="font-size:11px;color:#666;margin-top:4px">介绍合伙人</div></div>
<div style="width:1px;background:rgba(22,165,90,.2)"></div>
<div style="flex:1;text-align:center"><div style="font-size:22px;font-weight:800;color:#16A55A;line-height:1">8</div><div style="font-size:11px;color:#666;margin-top:4px">推荐企业</div></div>
<div style="width:1px;background:rgba(22,165,90,.2)"></div>
<div style="flex:1;text-align:center"><div style="font-size:22px;font-weight:800;color:#FF6B35;line-height:1">¥100</div><div style="font-size:11px;color:#FF6B35;font-weight:500;margin-top:4px">待领奖励</div></div>
</div>
<!-- 内部小 banner · 邀请成为合伙人 -->
<div style="margin-top:12px;background:linear-gradient(135deg,#5B7CFA 0%,#3651DB 100%);border-radius:12px;padding:12px 14px;display:flex;align-items:center;gap:10px;position:relative;overflow:hidden">
<div style="position:absolute;right:-15px;top:-20px;width:80px;height:80px;border-radius:50%;background:rgba(255,255,255,.1)"></div>
<div style="flex:1;position:relative">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px"><span style="font-size:13.5px;font-weight:700;color:#fff">邀请朋友成为合伙人</span><span style="background:#FFD86B;color:#7A4B00;font-size:10px;padding:1px 6px;border-radius:4px;font-weight:700">¥50/人</span></div>
<div style="font-size:11px;color:rgba(255,255,255,.85);line-height:1.5">分享专属海报 · 朋友首单成交您得礼金</div>
</div>
<div style="height:28px;padding:0 12px;background:#fff;color:#3651DB;border-radius:14px;display:flex;align-items:center;font-size:11.5px;font-weight:700;flex-shrink:0;position:relative">去邀请 ›</div>
</div>
</div>
<!-- 主推服务 -->
<div style="margin:14px 16px 0;background:linear-gradient(180deg,#FFF1E2,#FFFCF6);border-radius:16px;padding:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:6px"><svg width="16" height="16" viewBox="0 0 16 16" fill="#FFD86B" stroke="#E89B14" stroke-width="1.3" stroke-linejoin="round"><path d="M8 1l1.8 4 4.2.4-3.2 2.8 1 4.2L8 10.4l-3.8 2 1-4.2L1 5.4l4.2-.4L8 1z"></path></svg><span style="font-size:15px;font-weight:800;font-style:italic;color:#A8430F">主推服务</span></div>
<span style="font-size:11px;color:#A8430F;opacity:.7">公司严选 · 出单更稳</span>
</div>
<div style="background:#fff;border-radius:12px;padding:14px;margin-bottom:10px;display:flex;align-items:center;gap:12px">
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px"><span style="background:linear-gradient(135deg,#FF8A4E,#FF6B35);color:#fff;font-size:10px;padding:2px 7px;border-radius:5px;font-weight:600">🔥 高佣金</span><span style="font-size:15px;font-weight:700;color:#1A1A2E">个体工商户/公司注册</span></div>
<div style="font-size:12px;color:#888;margin-bottom:8px">开公司办执照,全程合规代办</div>
<div style="display:flex;gap:6px"><span class="tag-qbao">亲亲小保</span><span class="tag-qfu">个人创业支持服务</span></div>
</div>
<div style="text-align:center"><div style="font-size:24px;font-weight:800;color:#FF6B35;line-height:1">30%</div><div style="font-size:10px;color:#FF6B35;margin-top:2px">佣金服务费的</div><div style="margin-top:8px;background:linear-gradient(135deg,#FFB36A,#FF6B35);color:#fff;font-size:12px;font-weight:600;padding:5px 16px;border-radius:14px">查看</div></div>
</div>
<div style="text-align:center"><span style="font-size:12px;color:#A8430F;font-weight:500">更多服务 ⌄</span></div>
</div>
<!-- 亲亲企服 -->
<div style="margin:14px 16px 0">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px"><div style="display:flex;align-items:center;gap:7px"><div style="width:24px;height:24px;background:linear-gradient(135deg,#7B9AFF,#4A6CF7);border-radius:6px;display:flex;align-items:center;justify-content:center"><span style="color:#fff;font-size:13px;font-weight:700"></span></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">亲亲企服</span></div><span style="font-size:12px;color:#888">更多 ›</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">人事用工服务</div><div style="font-size:11px;color:#888">零基础可推</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">工商财税服务</div><div style="font-size:11px;color:#888">官方严选</div></div>
</div>
</div>
<!-- 亲亲小保 -->
<div style="margin:14px 16px 0;padding-bottom:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px"><div style="display:flex;align-items:center;gap:7px"><div style="width:24px;height:24px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:6px;display:flex;align-items:center;justify-content:center"><span style="color:#fff;font-size:13px;font-weight:700"></span></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">亲亲小保</span></div><span style="font-size:12px;color:#888">更多 ›</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">Offer分析/择业规划</div><div style="font-size:11px;color:#888">零基础可推</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">人社保/公积金/个税…</div><div style="font-size:11px;color:#FF6B35">高佣金</div></div>
</div>
</div>
</div>
<div class="home"><div class="home-bar"></div></div>
</div>
</div>
<!-- ================================================================
P4 · 申请成为合伙人(新表单)
================================================================ -->
<div style="position:absolute;left:1980px;top:120px;width:375px">
<div data-drags-parent="1" class="label">P4 · 申请成为合伙人</div>
<div class="phone" style="height: 812px">
<div class="statusbar">
<span class="stime">9:41</span>
<div class="sicons"><svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><circle cx="2" cy="9.5" r="1.5"></circle><circle cx="6.5" cy="9.5" r="1.5"></circle><circle cx="11" cy="9.5" r="1.5"></circle></svg><svg width="16" height="11" viewBox="0 0 16 11" fill="none"><circle cx="8" cy="9.5" r="1" fill="currentColor"></circle></svg><svg width="26" height="13" viewBox="0 0 26 13" fill="none"><rect x=".5" y="1.5" width="22" height="10" rx="2.5" stroke="currentColor" stroke-opacity=".4"></rect><rect x="2" y="3" width="19" height="7" rx="1.2" fill="currentColor"></rect></svg></div>
</div>
<div class="navbar">
<div class="back-ic"><svg width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M14 5l-6 6 6 6" stroke="#1A1A2E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div>
<div class="navtitle" style="padding-right: 0px">申请成为合伙人</div>
<div class="capsule"><svg width="18" height="4" viewBox="0 0 18 4"><circle cx="3" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="9" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="15" cy="2" r="1.5" fill="#1A1A2E"></circle></svg><div style="width:.5px;height:14px;background:rgba(0,0,0,.15)"></div><svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" stroke="#1A1A2E" stroke-width="1.1" fill="none"></circle><circle cx="8" cy="8" r="2.2" fill="#1A1A2E"></circle></svg></div>
</div>
<div class="scroll" style="padding:0 16px 20px">
<!-- Hero banner -->
<div style="position:relative;background:linear-gradient(135deg,#5B7CFA 0%,#3651DB 60%,#7B5FE8 100%);border-radius:18px;padding:18px 20px;margin-bottom:14px;overflow:hidden">
<div style="position:absolute;right:-30px;top:-30px;width:130px;height:130px;border-radius:50%;background:rgba(255,255,255,.08)"></div>
<div style="position:relative">
<div style="display:inline-block;background:rgba(255,255,255,.22);border-radius:10px;padding:3px 9px;font-size:11px;color:#fff;margin-bottom:8px">🎉 加入合伙人计划</div>
<div style="color:#fff;font-size:22px;font-weight:800;line-height:1.25;margin-bottom:6px">推荐企业客户<br>最高享 <span style="color:#FFD86B">30%</span> 持续返佣</div>
<div style="color:rgba(255,255,255,.85);font-size:12px;line-height:1.6">续费持续返佣 · 小程序提现秒到账</div>
</div>
</div>
<!-- Form card -->
<div style="background:#fff;border-radius:16px;padding:18px;box-shadow:0 4px 16px rgba(60,55,120,.06)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px"><div class="section-bar"></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">填写您的资料</span></div>
<div style="display:flex;flex-direction:column;gap:14px">
<!-- 姓名 -->
<div>
<div style="font-size:12px;color:#666;margin-bottom:8px">姓名<span style="color:#FF4D4F;margin-left:2px">*</span></div>
<div class="input">请输入您的真实姓名</div>
</div>
<!-- 手机号 + 验证码 -->
<div>
</div>
<div style="margin-top:-6px">
</div>
<!-- 身份证号 -->
<div>
<div style="font-size:12px;color:#666;margin-bottom:8px">身份证号</div>
<div class="input">请输入您的身份证号(18位)</div>
</div>
<!-- 身份类别 -->
<div>
<div style="font-size:12px;color:#666;margin-bottom:10px">身份类别<span style="color:#FF4D4F;margin-left:2px">*</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
<div class="role-chip active">HR</div>
<div class="role-chip">财务</div>
<div class="role-chip">老板</div>
<div class="role-chip">猎头</div>
<div class="role-chip">博主</div>
<div class="role-chip">其他</div>
</div>
</div>
<!-- 介绍人 -->
<div>
</div>
</div>
</div>
<!-- 合伙人特权 (no 介绍人 reward mention) -->
<div style="background:linear-gradient(135deg,#FFF5EC,#FFEFE0);border-radius:14px;padding:14px 16px;margin-top:12px;display:flex;align-items:center;gap:12px">
<div style="width:36px;height:36px;background:linear-gradient(135deg,#FF8A4E,#FF6B35);border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg width="20" height="20" viewBox="0 0 20 20" fill="#fff"><path d="M10 1l2.5 5.5 6 .5-4.5 4 1.5 6L10 14l-5.5 3 1.5-6L1.5 7l6-.5L10 1z"></path></svg>
</div>
<div style="flex:1">
<div style="font-size:13px;font-weight:600;color:#A8430F;line-height:1.5">合伙人特权</div>
<div style="font-size:11px;color:#C26033;margin-top:2px;line-height:1.5">订单 30% 持续返佣 · 续费持续 · 小程序秒到账</div>
</div>
</div>
<!-- Agreement -->
<div style="display:flex;align-items:flex-start;gap:8px;margin-top:18px;padding:0 4px">
<div style="width:18px;height:18px;border-radius:50%;background:#4D6FF5;flex-shrink:0;margin-top:1px;display:flex;align-items:center;justify-content:center"><svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4l3 3 5-6" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"></path></svg></div>
<span style="font-size:12px;color:#666;line-height:1.6">我已阅读并同意 <span style="color:#4D6FF5">《合伙人加盟协议》</span> · <span style="color:#4D6FF5">《隐私政策》</span></span>
</div>
<div style="margin-top:18px" class="btn-primary">提交申请</div>
</div>
<div class="home"><div class="home-bar"></div></div>
</div>
</div>
<!-- ===== 关键逻辑注释 ===== -->
<style>
.logic-card { position:absolute; top:962px; width:375px; background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 4px 14px rgba(60,55,120,.08); border:1px solid rgba(77,111,245,.12); font-family:'Noto Sans SC',sans-serif; }
.logic-card .lc-title { display:flex; align-items:center; gap:8px; margin-bottom:10px; font-size:13px; font-weight:700; color:#1A1A2E; }
.logic-card .lc-dot { width:6px; height:6px; border-radius:50%; background:#4D6FF5; flex-shrink:0; }
.logic-card .lc-tag { font-size:10px; font-weight:600; padding:2px 7px; border-radius:5px; background:#EAF0FE; color:#4A6CF7; margin-left:auto; }
.logic-card ul { list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:7px; }
.logic-card li { font-size:11.5px; line-height:1.55; color:#4A4866; padding-left:14px; position:relative; }
.logic-card li::before { content:''; position:absolute; left:2px; top:7px; width:4px; height:4px; border-radius:50%; background:#B5B3CD; }
.logic-card li b { color:#1A1A2E; font-weight:600; }
.logic-card .lc-sub { font-size:10.5px; color:#7775A0; margin-top:8px; padding-top:8px; border-top:1px dashed #EFEDF6; line-height:1.5; }
</style>
<!-- P1 · 未登录首页 -->
<div class="logic-card" style="left:80px">
<div class="lc-title"><span class="lc-dot"></span>P1 · 未登录首页 · 关键逻辑<span class="lc-tag">未登录态</span></div>
<ul>
<li><b>入口</b>:用户分享链接 / 海报扫码 / 小程序首页直接进入</li>
<li><b>核心目标</b>:让访客 30 秒内理解「亲亲创客是什么」并完成登录</li>
<li><b>Hero 双 CTA</b>:「一键登录」→ 跳转 P2 登录页;「申请入驻」→ 同样先要求登录,登录后跳 P4 申请表单</li>
<li><b>主推服务/企服/小保</b>:可浏览,但点击「查看」时若未登录则唤起登录浮层</li>
</ul>
<div class="lc-sub">登录后根据用户角色路由:合伙人 → 合伙人首页;非合伙人 → P3a/P3b;员工 → 员工视图</div>
</div>
<!-- P2 · 登录页 -->
<div class="logic-card" style="left:555px">
<div class="lc-title"><span class="lc-dot"></span>P2 · 登录页 · 关键逻辑<span class="lc-tag">登录</span></div>
<ul>
<li><b>默认登录方式</b>:手机号一键登录(基于微信授权)</li>
<li><b>协议勾选</b>:未勾选时点击登录按钮 → 弹出协议确认弹窗,二次确认后才发起登录</li>
<li><b>其他登录方式</b>:手机号、验证码登录</li>
<li><b>登录成功后路由</b>:新用户 → P3a 非介绍人首页;已是介绍人 → P3b;已是合伙人 → 合伙人首页</li>
<li><b>暂不登录</b>:返回 P1 未登录首页继续浏览</li>
</ul>
<div class="lc-sub">手机号校验失败 / 验证码错误 → toast 提示;同一手机号 60s 内仅可发送 1 次验证码</div>
</div>
<!-- P3a · 非介绍人状态 -->
<div class="logic-card" style="left:1030px">
<div class="lc-title"><span class="lc-dot"></span>P3a · 非介绍人 · 关键逻辑<span class="lc-tag">已登录 · 普通用户</span></div>
<ul>
<li><b>身份判定</b>:登录用户既非合伙人,也未推荐过任何合伙人 → 显示该态</li>
<li><b>Hero 申请合伙人</b>:点击「立即申请」→ P4 申请表单</li>
<li><b>邀请朋友成为合伙人</b>:点击「去分享」→ 唤起分享面板(海报 / 微信 / 链接),生成带本人 ID 的专属邀请链接</li>
<li><b>分享后</b>:被邀请人通过该链接注册并申请成合伙人 → 本人自动升级为「介绍人」,下次进入显示 P3b</li>
<li><b>主推/企服/小保</b>:可浏览详情,点击「查看」进入服务详情页</li>
</ul>
<div class="lc-sub">奖励规则:被邀请合伙人首单成交,介绍人得 ¥50 福礼金(由渠道管理员线下审核发放)</div>
</div>
<!-- P3b · 介绍人状态 -->
<div class="logic-card" style="left:1505px">
<div class="lc-title"><span class="lc-dot"></span>P3b · 介绍人 · 关键逻辑<span class="lc-tag">已登录 · 介绍人</span></div>
<ul>
<li><b>身份判定</b>:登录用户曾推荐过至少 1 位合伙人 (无论是否成单) → 自动显示「介绍人」徽章</li>
<li><b>邀请合伙人板块</b>:展示介绍合伙人数 / 推荐企业数 / 待领奖励金额;点击进入「介绍人业务详情」(见 03 介绍人视图)</li>
<li><b>板块内嵌小 banner</b>:「邀请朋友成为合伙人」点击同样唤起分享面板,引导持续邀请</li>
<li><b>Hero 申请合伙人</b>:保留入口;介绍人本人仍可申请成为合伙人(身份不互斥)</li>
<li><b>待领提示</b>:右上角徽标显示「N 笔待领」,点击直达介绍人业务详情</li>
</ul>
<div class="lc-sub">介绍人 ↔ 合伙人 ↔ 普通用户身份独立,可同时具备;显示优先级:合伙人 &gt; 介绍人 &gt; 普通</div>
</div>
<!-- P4 · 申请成为合伙人 -->
<div class="logic-card" style="left:1980px">
<div class="lc-title"><span class="lc-dot"></span>P4 · 申请成为合伙人 · 关键逻辑<span class="lc-tag">提交申请</span></div>
<ul>
<li><b>必填校验</b>:姓名 / 手机号 / 短信验证码 / 身份证号 (18 位校验) / 身份类别</li>
<li><b>手机号</b>:默认带入登录手机号,可修改;修改后需重新获取验证码</li>
<li><b>身份类别</b>:单选 HR / 财务 / 老板 / 猎头 / 博主 / 其他(用于后台分群运营)</li>
<li><b>介绍人(选填)</b>:填写姓名 → 提交后由渠道管理员匹配并绑定;也可来自邀请链接自动带入并锁定</li>
<li><b>提交后</b>:进入「审核中」状态,预计 1-3 个工作日;审核通过后用户升级为合伙人,下次进入显示合伙人首页</li>
</ul>
<div class="lc-sub">审核期间该用户仍可正常浏览,但暂不能产生订单佣金;审核结果通过站内通知 + 短信告知</div>
</div>
</x-dc>
</body>
</html>
This diff could not be displayed because it is too large.
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<meta name="design_doc_mode" content="canvas">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Noto Sans SC', -apple-system, 'PingFang SC', sans-serif; -webkit-font-smoothing: antialiased; }
.phone { width:375px; height:812px; background:linear-gradient(180deg,#E8EAFA 0%,#EEE9F6 50%,#F4F1FA 100%); border-radius:42px; overflow:hidden; box-shadow:0 24px 50px rgba(60,55,120,.18),0 4px 14px rgba(0,0,0,.08); position:relative; display:flex; flex-direction:column; }
.statusbar { height:44px; display:flex; align-items:center; padding:0 28px 0 32px; flex-shrink:0; z-index:3; }
.stime { font:600 16px/1 -apple-system,sans-serif; flex:1; color:#1A1A2E; }
.sicons { display:flex; gap:5px; align-items:center; color:#1A1A2E; }
.navbar { height:44px; display:flex; align-items:center; padding:0 6px 0 16px; flex-shrink:0; z-index:2; }
.navtitle { flex:1; text-align:center; font:600 17px/1 'Noto Sans SC',sans-serif; color:#1A1A2E; padding-right:88px; padding-left:32px; }
.capsule { width:87px; height:32px; background:rgba(255,255,255,.7); border-radius:16px; border:.5px solid rgba(0,0,0,.08); display:flex; align-items:center; justify-content:space-around; flex-shrink:0; }
.scroll { flex:1; overflow-y:auto; }
.scroll::-webkit-scrollbar { width:3px; }
.scroll::-webkit-scrollbar-thumb { background:rgba(0,0,0,.1); border-radius:2px; }
.home { height:34px; display:flex; align-items:flex-end; justify-content:center; padding-bottom:8px; flex-shrink:0; }
.home-bar { width:134px; height:5px; background:#1A1A2E; border-radius:3px; }
.back-ic { width:28px; height:28px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.label { position:absolute; top:-26px; font:500 12px/1 -apple-system,sans-serif; color:rgba(40,30,80,.55); letter-spacing:.3px; }
.section-bar { width:3px; height:14px; background:#4A6CF7; border-radius:2px; }
.card { background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 2px 10px rgba(60,55,120,.06); }
.sicons-set svg:first-child + svg + svg { font-size: inherit; }
</style>
</helmet>
<!-- ===== CANVAS HEADER ===== -->
<div style="position:absolute;left:80px;top:14px;font:800 22px/1 'Noto Sans SC',sans-serif;color:#1A1A2E;letter-spacing:-.4px">员工 · 介绍人视图</div>
<div style="position:absolute;left:80px;top:46px;display:flex;align-items:center;gap:10px;font:500 12px/1 'Noto Sans SC',sans-serif;color:rgba(40,30,80,.6)">
<span style="display:inline-block;width:18px;height:2px;background:#16A55A"></span>
<span>内部员工(非对接人) · 介绍人业务 + 我介绍的合伙人 + 合伙人详情</span>
</div>
<!-- ================================================================
P1 · 员工首页(介绍人状态)
================================================================ -->
<div style="position:absolute;left:80px;top:120px;width:375px">
<div data-drags-parent="1" class="label">P1 · 员工首页(介绍人)</div>
<div class="phone">
<div class="statusbar">
<span class="stime">9:41</span>
<div class="sicons"><svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><circle cx="2" cy="9.5" r="1.5"></circle><circle cx="6.5" cy="9.5" r="1.5"></circle><circle cx="11" cy="9.5" r="1.5"></circle></svg><svg width="16" height="11" viewBox="0 0 16 11" fill="none"><circle cx="8" cy="9.5" r="1" fill="currentColor"></circle></svg><svg width="26" height="13" viewBox="0 0 26 13" fill="none"><rect x=".5" y="1.5" width="22" height="10" rx="2.5" stroke="currentColor" stroke-opacity=".4"></rect><rect x="2" y="3" width="19" height="7" rx="1.2" fill="currentColor"></rect></svg></div>
</div>
<div class="navbar">
<div style="flex:1"></div>
<div class="capsule"><svg width="18" height="4" viewBox="0 0 18 4"><circle cx="3" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="9" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="15" cy="2" r="1.5" fill="#1A1A2E"></circle></svg><div style="width:.5px;height:14px;background:rgba(0,0,0,.15)"></div><svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" stroke="#1A1A2E" stroke-width="1.1" fill="none"></circle><circle cx="8" cy="8" r="2.2" fill="#1A1A2E"></circle></svg></div>
</div>
<div class="scroll">
<!-- Profile · 介绍人 -->
<div style="padding:6px 18px 14px;display:flex;align-items:center;gap:12px">
<div style="width:54px;height:54px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:2px solid #fff;box-shadow:0 4px 12px rgba(22,165,90,.28)">
<span style="color:#fff;font-size:22px;font-weight:700"></span>
</div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px"><span style="font-size:18px;font-weight:700;color:#1A1A2E">杨恒宇</span><span style="background:linear-gradient(135deg,#6CD3A4,#16A55A);color:#fff;font-size:11px;padding:2px 9px;border-radius:10px;font-weight:600">介绍人</span></div>
<div style="font-size:13px;color:#666">159****1234&nbsp;</div>
</div>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M6 3l6 6-6 6" stroke="#999" stroke-width="1.6" stroke-linecap="round"></path></svg>
</div>
<!-- HERO · 申请合伙人 -->
<div style="margin:0 16px 14px;background:linear-gradient(135deg,#5B7CFA 0%,#3651DB 60%,#7B5FE8 100%);border-radius:18px;padding:18px;color:#fff;position:relative;overflow:hidden">
<div style="position:absolute;right:-30px;top:-30px;width:130px;height:130px;border-radius:50%;background:rgba(255,255,255,.1)"></div>
<div style="position:absolute;right:14px;bottom:-30px;width:80px;height:80px;border-radius:50%;background:rgba(255,255,255,.08)"></div>
<div style="position:relative">
<div style="display:inline-flex;align-items:center;gap:5px;background:rgba(255,255,255,.22);border-radius:10px;padding:3px 9px;font-size:11px;margin-bottom:9px">🎉 你还不是合伙人</div>
<div style="font-size:22px;font-weight:800;line-height:1.2;margin-bottom:6px">申请成为合伙人<br>最高享 <span style="color:#FFD86B">30%</span> 持续返佣</div>
<div style="font-size:12.5px;color:rgba(255,255,255,.85);line-height:1.6;margin-bottom:14px">推荐企业客户 · 订单成交即得佣金 · 提现秒到账</div>
<div style="display:inline-flex;align-items:center;gap:6px;background:#fff;color:#3651DB;font-size:14px;font-weight:700;padding:9px 22px;border-radius:18px;box-shadow:0 6px 14px rgba(0,0,0,.15)">立即申请 ›</div>
</div>
</div>
<!-- 介绍人业务 板块 -->
<div style="margin:0 16px 0;background:linear-gradient(135deg,#E5F7EC 0%,#D1F0DC 50%,#C7EFE2 100%);border-radius:18px;padding:16px;position:relative;overflow:hidden;border:1.5px solid rgba(22,165,90,.18);box-shadow:0 8px 24px rgba(22,165,90,.12)">
<div style="position:absolute;right:-20px;top:-20px;width:100px;height:100px;border-radius:50%;background:radial-gradient(circle,rgba(108,211,164,.4),transparent 70%)"></div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;position:relative">
<div style="width:38px;height:38px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:11px;display:flex;align-items:center;justify-content:center;box-shadow:0 6px 14px rgba(22,165,90,.32)">
<svg width="20" height="20" viewBox="0 0 22 22" fill="none"><path d="M8 14a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6-2a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM2 20c0-3.3 2.7-6 6-6s6 2.7 6 6m1.5-5.5c2.5 0 4.5 2 4.5 4.5" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</div>
<div style="flex:1">
<div style="font-size:16px;font-weight:800;color:#0E5A30;letter-spacing:-.2px">邀请合伙人</div>
<div style="font-size: 11px; color: #1A8C4D; margin-top: 2px">推荐他人成为合伙人 · 首单成交奖¥50</div>
</div>
<span style="background:#FF8145;color:#fff;font-size:10px;padding:3px 8px;border-radius:9px;font-weight:600">2 笔待领</span>
</div>
<div style="background:rgba(255,255,255,.78);border-radius:12px;padding:12px 0;display:flex;backdrop-filter:blur(8px);position:relative">
<div style="flex:1;text-align:center"><div style="font-size:22px;font-weight:800;color:#16A55A;line-height:1">3</div><div style="font-size:11px;color:#666;margin-top:4px">介绍合伙人</div></div>
<div style="width:1px;background:rgba(22,165,90,.2)"></div>
<div style="flex:1;text-align:center"><div style="font-size:22px;font-weight:800;color:#16A55A;line-height:1">8</div><div style="font-size:11px;color:#666;margin-top:4px">推荐企业</div></div>
<div style="width:1px;background:rgba(22,165,90,.2)"></div>
<div style="flex:1;text-align:center"><div style="font-size:22px;font-weight:800;color:#FF6B35;line-height:1">¥100</div><div style="font-size:11px;color:#FF6B35;font-weight:500;margin-top:4px">待领奖励</div></div>
</div>
<!-- 内部小 banner · 邀请成为合伙人 -->
<div style="margin-top:12px;background:linear-gradient(135deg,#5B7CFA 0%,#3651DB 100%);border-radius:12px;padding:12px 14px;display:flex;align-items:center;gap:10px;position:relative;overflow:hidden">
<div style="position:absolute;right:-15px;top:-20px;width:80px;height:80px;border-radius:50%;background:rgba(255,255,255,.1)"></div>
<div style="flex:1;position:relative">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px"><span style="font-size:13.5px;font-weight:700;color:#fff">邀请朋友成为合伙人</span><span style="background:#FFD86B;color:#7A4B00;font-size:10px;padding:1px 6px;border-radius:4px;font-weight:700">¥50/人</span></div>
<div style="font-size:11px;color:rgba(255,255,255,.85);line-height:1.5">分享专属海报 · 朋友首单成交您得礼金</div>
</div>
<div style="height:28px;padding:0 12px;background:#fff;color:#3651DB;border-radius:14px;display:flex;align-items:center;font-size:11.5px;font-weight:700;flex-shrink:0;position:relative">去邀请 ›</div>
</div>
</div>
<!-- 主推服务 -->
<div style="margin:14px 16px 0;background:linear-gradient(180deg,#FFF1E2,#FFFCF6);border-radius:16px;padding:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:6px"><svg width="16" height="16" viewBox="0 0 16 16" fill="#FFD86B" stroke="#E89B14" stroke-width="1.3" stroke-linejoin="round"><path d="M8 1l1.8 4 4.2.4-3.2 2.8 1 4.2L8 10.4l-3.8 2 1-4.2L1 5.4l4.2-.4L8 1z"></path></svg><span style="font-size:15px;font-weight:800;font-style:italic;color:#A8430F;letter-spacing:.5px">主推服务</span></div>
<span style="font-size:11px;color:#A8430F;opacity:.7">公司严选好品 · 出单更稳更省心</span>
</div>
<div style="background:#fff;border-radius:12px;padding:14px;margin-bottom:10px;display:flex;align-items:center;gap:12px">
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px"><span style="background:linear-gradient(135deg,#FF8A4E,#FF6B35);color:#fff;font-size:10px;padding:2px 7px;border-radius:5px;font-weight:600">🔥 高佣金</span><span style="font-size:15px;font-weight:700;color:#1A1A2E">个体工商户/公司注册</span></div>
<div style="font-size:12px;color:#888;margin-bottom:8px">开公司办执照,全程合规代办</div>
<div style="display:flex;gap:6px"><span style="background:#E5F7EC;color:#16A55A;font-size:11px;padding:2px 7px;border-radius:4px;font-weight:500">亲亲小保</span><span style="background:#E8EEFE;color:#4A6CF7;font-size:11px;padding:2px 7px;border-radius:4px;font-weight:500">个人创业支持服务</span></div>
</div>
<div style="text-align:center"><div style="font-size:24px;font-weight:800;color:#FF6B35;line-height:1">30%</div><div style="font-size:10px;color:#FF6B35;margin-top:2px">合伙人佣金</div><div style="margin-top:8px;background:linear-gradient(135deg,#FFB36A,#FF6B35);color:#fff;font-size:12px;font-weight:600;padding:5px 16px;border-radius:14px">查看</div></div>
</div>
<div style="background:#fff;border-radius:12px;padding:14px;display:flex;align-items:center;gap:12px">
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px"><span style="background: #EAF0FE; color: #4A6CF7; border: 1px solid #BFD0FA; font-size: 10px; padding: 2px 7px; border-radius: 5px; font-weight: 600; width: 70px">👍 易成交</span><span style="font-size:15px;font-weight:700;color:#1A1A2E">虚拟注册地/租赁注册地址</span></div>
<div style="font-size:12px;color:#888;margin-bottom:8px">全流程代办零操心</div>
<div style="display:flex;gap:6px"><span style="background:#E8EEFE;color:#4A6CF7;font-size:11px;padding:2px 7px;border-radius:4px;font-weight:500">亲亲企服</span><span style="background:#E8EEFE;color:#4A6CF7;font-size:11px;padding:2px 7px;border-radius:4px;font-weight:500">人事用工服务</span></div>
</div>
<div style="text-align:center"><div style="font-size:24px;font-weight:800;color:#FF6B35;line-height:1">10%</div><div style="font-size:10px;color:#FF6B35;margin-top:2px">合伙人佣金</div><div style="margin-top:8px;background:linear-gradient(135deg,#FFB36A,#FF6B35);color:#fff;font-size:12px;font-weight:600;padding:5px 16px;border-radius:14px">查看</div></div>
</div>
<div style="text-align:center;margin-top:10px"><span style="font-size:12px;color:#A8430F;font-weight:500">更多服务 ⌄</span></div>
</div>
<!-- 亲亲企服 -->
<div style="margin:14px 16px 0">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="display:flex;align-items:center;gap:7px"><div style="width:24px;height:24px;background:linear-gradient(135deg,#7B9AFF,#4A6CF7);border-radius:6px;display:flex;align-items:center;justify-content:center"><span style="color:#fff;font-size:13px;font-weight:700"></span></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">亲亲企服</span></div>
<span style="font-size:12px;color:#888">更多服务 ›</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">人事用工服务</div><div style="font-size:11px;color:#888">零基础可推</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">工商财税服务</div><div style="font-size:11px;color:#888">官方严选</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">企业出海服务</div><div style="font-size:11px;color:#4A6CF7">全新上线</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">数字员工服务</div><div style="font-size:11px;color:#16A55A">新品首发</div></div>
</div>
</div>
<!-- 亲亲小保 -->
<div style="margin:14px 16px 0;padding-bottom:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="display:flex;align-items:center;gap:7px"><div style="width:24px;height:24px;background:linear-gradient(135deg,#6CD3A4,#16A55A);border-radius:6px;display:flex;align-items:center;justify-content:center"><span style="color:#fff;font-size:13px;font-weight:700"></span></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">亲亲小保</span></div>
<span style="font-size:12px;color:#888">更多服务 ›</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">Offer分析/择业规划</div><div style="font-size:11px;color:#888">零基础可推</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">个体工商户/公司注册</div><div style="font-size:11px;color:#888">官方严选</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">人社保/公积金/个税…</div><div style="font-size:11px;color:#FF6B35">高佣金</div></div>
<div style="background:#fff;border-radius:12px;padding:12px 14px"><div style="font-size:13px;font-weight:600;color:#1A1A2E;margin-bottom:3px">临时挂靠参保</div><div style="font-size:11px;color:#888">个人刚需</div></div>
</div>
</div>
</div>
<!-- Tab bar -->
<div class="home"><div class="home-bar"></div></div>
</div>
</div>
<!-- ================================================================
P2 · 介绍人业务详情
================================================================ -->
<div style="position:absolute;left:555px;top:120px;width:375px">
<div data-drags-parent="1" class="label">P2 · 介绍人业务详情</div>
<div class="phone">
<div class="statusbar">
<span class="stime">9:41</span>
<div class="sicons"><svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><circle cx="2" cy="9.5" r="1.5"></circle><circle cx="6.5" cy="9.5" r="1.5"></circle><circle cx="11" cy="9.5" r="1.5"></circle></svg><svg width="16" height="11" viewBox="0 0 16 11" fill="none"><circle cx="8" cy="9.5" r="1" fill="currentColor"></circle></svg><svg width="26" height="13" viewBox="0 0 26 13" fill="none"><rect x=".5" y="1.5" width="22" height="10" rx="2.5" stroke="currentColor" stroke-opacity=".4"></rect><rect x="2" y="3" width="19" height="7" rx="1.2" fill="currentColor"></rect></svg></div>
</div>
<div class="navbar">
<div class="back-ic"><svg width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M14 5l-6 6 6 6" stroke="#1A1A2E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div>
<div class="navtitle" style="padding-right: 0px">邀请合伙人</div>
<div class="capsule"><svg width="18" height="4" viewBox="0 0 18 4"><circle cx="3" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="9" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="15" cy="2" r="1.5" fill="#1A1A2E"></circle></svg><div style="width:.5px;height:14px;background:rgba(0,0,0,.15)"></div><svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" stroke="#1A1A2E" stroke-width="1.1" fill="none"></circle><circle cx="8" cy="8" r="2.2" fill="#1A1A2E"></circle></svg></div>
</div>
<div class="scroll" style="padding:0 16px 16px">
<!-- Rule banner -->
<div style="background:linear-gradient(135deg,#16A55A 0%,#0E7A40 100%);border-radius:14px;padding:14px 16px;color:#fff;margin-bottom:12px;position:relative;overflow:hidden">
<div style="position:absolute;right:-10px;bottom:-20px;width:90px;height:90px;border-radius:50%;background:rgba(255,255,255,.1)"></div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;position:relative"><svg width="16" height="16" viewBox="0 0 16 16" fill="#FFD86B"><path d="M8 1l1.8 4 4.2.4-3.2 2.8 1 4.2L8 10.4l-3.8 2 1-4.2L1 5.4l4.2-.4L8 1z"></path></svg><span style="font-size:13px;font-weight:600">介绍人奖励规则</span></div>
<div style="font-size:12px;line-height:1.65;color:rgba(255,255,255,.92)">您推荐的合伙人,每个合伙人 <b style="color:#FFD86B">首单成交</b> 即可获得 <b style="color:#FFD86B">¥50</b> 福礼金。奖励由渠道管理员审核后线下发放。</div>
</div>
<!-- 2-stat grid -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px">
<div class="card" style="padding:16px;position:relative">
<div style="display:flex;align-items:flex-end;justify-content:space-between">
<div>
<div style="display:flex;align-items:baseline;gap:3px;line-height:1"><span style="font-size:28px;font-weight:800;color:#16A55A">4</span><span style="font-size:13px;color:#888"></span></div>
<div style="font-size:12px;color:#888;margin-top:6px">我介绍的合伙人</div>
</div>
<div style="display:flex"></div>
</div>
<div style="margin-top:10px;padding-top:10px;border-top:1px dashed #EFEDF6;display:flex;align-items:center;gap:10px;font-size:11px">
<span style="display:inline-flex;align-items:center;gap:4px;color:#16A55A"><span style="display: inline-block; width: 5px; height: 5px; background: rgb(22, 165, 90); border-radius: 50%;"></span>已成单 3</span>
<span style="display:inline-flex;align-items:center;gap:4px;color:#E89B14"><span style="display:inline-block;width:5px;height:5px;background:#E89B14;border-radius:50%"></span>未成单 1</span>
</div>
</div>
<div style="background:linear-gradient(135deg,#FFF1E2,#FFEAD1);border-radius:14px;padding:16px;border:1.5px solid #FFD8B8">
<div style="font-size:11px;color:#A8430F;margin-bottom:4px">奖励(元)</div>
<div style="display:flex;align-items:baseline;gap:6px;line-height:1"><span style="font-size:28px;font-weight:800;color:#FF6B35">150</span><span style="font-size:11px;color:#A8430F">累计获取</span></div>
<div style="margin-top:8px;padding-top:8px;border-top:1px dashed rgba(255,107,53,.25);display:flex;align-items:center;justify-content:space-between"><span style="font-size:11px;color:#A8430F">待发放</span><span style="font-size:14px;font-weight:700;color:#FF6B35">¥100 (2笔)</span></div>
</div>
</div>
<!-- 我介绍的合伙人 -->
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="display:flex;align-items:center;gap:8px"><div class="section-bar" style="background:#16A55A"></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">我介绍的合伙人</span></div>
<div style="font-size:12px;color:#888;display:flex;align-items:center;gap:4px">全部 <svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M2 3.5l3 3 3-3" stroke="#999" stroke-width="1.4" stroke-linecap="round"></path></svg></div>
</div>
<div style="display:flex;flex-direction:column;gap:10px">
<div class="card" style="padding:14px 16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
<div style="width:36px;height:36px;background:linear-gradient(135deg,#7B9AFF,#4A6CF7);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:15px;font-weight:600"></div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px"><span style="font-size:14px;font-weight:600;color:#1A1A2E">王辉</span></div>
<div style="font-size:11px;color:#999;margin-top:2px">2025-03-15 加入 | 159****1234</div>
</div>
</div>
<div style="background:#F8F8FC;border-radius:8px;padding:10px 12px;display:flex">
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">推荐企业</div><div style="font-size:14px;font-weight:700;color:#1A1A2E;margin-top:2px">3 家</div></div>
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">已成单</div><div style="font-size:14px;font-weight:700;color:#16A55A;margin-top:2px">2 家</div></div>
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">奖励状态</div><div style="font-size:12px;font-weight:600;color:#FF7A2E;margin-top:2px">待发放</div></div>
</div>
</div>
<div class="card" style="padding:14px 16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
<div style="width:36px;height:36px;background:linear-gradient(135deg,#FFB36A,#FF6B35);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:15px;font-weight:600"></div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px"><span style="font-size:14px;font-weight:600;color:#1A1A2E">李华</span></div>
<div style="font-size:11px;color:#999;margin-top:2px">2025-04-20 加入 | 137****5678</div>
</div>
</div>
<div style="background:#F8F8FC;border-radius:8px;padding:10px 12px;display:flex">
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">推荐企业</div><div style="font-size:14px;font-weight:700;color:#1A1A2E;margin-top:2px">3 家</div></div>
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">已成单</div><div style="font-size:14px;font-weight:700;color:#16A55A;margin-top:2px">2 家</div></div>
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">奖励状态</div><div style="font-size:12px;font-weight:600;color:#16A55A;margin-top:2px">已发放</div></div>
</div>
</div>
<div class="card" style="padding:14px 16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
<div style="width:36px;height:36px;background:linear-gradient(135deg,#A6B8F8,#7B5FE8);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:15px;font-weight:600"></div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:6px"><span style="font-size:14px;font-weight:600;color:#1A1A2E">赵明</span></div>
<div style="font-size:11px;color:#999;margin-top:2px">2025-05-12 加入 | 138****9023</div>
</div>
</div>
<div style="background:#F8F8FC;border-radius:8px;padding:10px 12px;display:flex">
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">推荐企业</div><div style="font-size:14px;font-weight:700;color:#1A1A2E;margin-top:2px">2 家</div></div>
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">已成单</div><div style="font-size:14px;font-weight:700;color:#999;margin-top:2px">0 家</div></div>
<div style="flex:1;text-align:center"><div style="font-size:11px;color:#888">奖励状态</div><div style="font-size:12px;font-weight:600;color:#999;margin-top:2px">无奖励</div></div>
</div>
</div>
</div>
</div>
</div>
<div class="home"><div class="home-bar"></div></div>
</div>
</div>
<!-- ================================================================
P3 · 合伙人详情
================================================================ -->
<div style="position:absolute;left:1030px;top:120px;width:375px">
<div data-drags-parent="1" class="label">P3 · 合伙人详情</div>
<div class="phone" style="height: 812px">
<div class="statusbar">
<span class="stime">9:41</span>
<div class="sicons"><svg width="17" height="11" viewBox="0 0 17 11" fill="currentColor"><circle cx="2" cy="9.5" r="1.5"></circle><circle cx="6.5" cy="9.5" r="1.5"></circle><circle cx="11" cy="9.5" r="1.5"></circle></svg><svg width="16" height="11" viewBox="0 0 16 11" fill="none"><circle cx="8" cy="9.5" r="1" fill="currentColor"></circle></svg><svg width="26" height="13" viewBox="0 0 26 13" fill="none"><rect x=".5" y="1.5" width="22" height="10" rx="2.5" stroke="currentColor" stroke-opacity=".4"></rect><rect x="2" y="3" width="19" height="7" rx="1.2" fill="currentColor"></rect></svg></div>
</div>
<div class="navbar">
<div class="back-ic"><svg width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M14 5l-6 6 6 6" stroke="#1A1A2E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg></div>
<div class="navtitle" style="padding-right: 0px">合伙人详情</div>
<div class="capsule"><svg width="18" height="4" viewBox="0 0 18 4"><circle cx="3" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="9" cy="2" r="1.5" fill="#1A1A2E"></circle><circle cx="15" cy="2" r="1.5" fill="#1A1A2E"></circle></svg><div style="width:.5px;height:14px;background:rgba(0,0,0,.15)"></div><svg width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.5" stroke="#1A1A2E" stroke-width="1.1" fill="none"></circle><circle cx="8" cy="8" r="2.2" fill="#1A1A2E"></circle></svg></div>
</div>
<div class="scroll" style="padding:0 16px 16px">
<!-- Partner header card -->
<div style="background:linear-gradient(135deg,#5B7CFA 0%,#3651DB 60%,#7B5FE8 100%);border-radius:16px;padding:18px;color:#fff;margin-bottom:12px;position:relative;overflow:hidden">
<div style="position:absolute;right:-20px;top:-30px;width:120px;height:120px;border-radius:50%;background:rgba(255,255,255,.1)"></div>
<div style="position:relative;display:flex;align-items:center;gap:14px">
<div style="width:56px;height:56px;background:rgba(255,255,255,.22);border-radius:50%;display:flex;align-items:center;justify-content:center;border:2px solid rgba(255,255,255,.45);font-size:22px;font-weight:700"></div>
<div style="flex:1">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:5px"><span style="font-size:18px;font-weight:700">王辉</span></div>
<div style="font-size:12px;color:rgba(255,255,255,.85);line-height:1.65">159****1234<br>2025-03-15 加入 | 已加入 105 天</div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;flex-shrink:0">
</div>
</div>
<div style="margin-top:14px;padding-top:14px;border-top:1px solid rgba(255,255,255,.18);display:flex">
<div style="flex:1;text-align:center"><div style="font-size:11px;color:rgba(255,255,255,.75)">推荐企业数</div><div style="font-size:20px;font-weight:800;margin-top:3px">3</div></div>
<div style="flex:1;text-align:center;border-left:1px solid rgba(255,255,255,.15)"><div style="font-size:11px;color:rgba(255,255,255,.75)">已成单企业数</div><div style="font-size:20px;font-weight:800;margin-top:3px">2</div></div>
</div>
</div>
<!-- 我获得的奖励 -->
<div style="background:linear-gradient(135deg,#FFF1E2,#FFEAD1);border-radius:14px;padding:14px 16px;border:1.5px solid #FFD8B8;margin-bottom:14px">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<div style="font-size:12px;color:#A8430F;margin-bottom:4px">我从 TA 获得的介绍奖励</div>
<div style="display:flex;align-items:baseline;gap:8px;line-height:1"><span style="font-size:26px;font-weight:800;color:#FF6B35">¥50</span></div>
</div>
<span style="background:#FFEEE0;color:#FF7A2E;font-size:12px;padding:5px 12px;border-radius:8px;font-weight:600">奖励已发放</span>
</div>
<div style="margin-top:10px;padding-top:10px;border-top:1px dashed rgba(255,107,53,.25);font-size:11px;color:#A8430F;line-height:1.5">首单触发:<b>北京卓思教育集团</b><div>2025-06-15 · 由渠道管理员线下发放</div></div>
</div>
<!-- 推荐的企业 -->
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
<div style="display:flex;align-items:center;gap:8px"><div class="section-bar"></div><span style="font-size:15px;font-weight:700;color:#1A1A2E">推荐的企业</span><span style="font-size:11px;color:#888">共 3 家</span></div>
<div style="display:flex;gap:6px;font-size:11px">
<span style="background:#EAF0FE;color:#4A6CF7;padding:3px 9px;border-radius:5px;border:1px solid #BFD0FA;font-weight:500">全部 3</span>
<span style="background:#fff;color:#16A55A;padding:3px 9px;border-radius:5px;border:1px solid #C7E8D2">已成单 2</span>
<span style="background:#fff;color:#E89B14;padding:3px 9px;border-radius:5px;border:1px solid #F0E2BB">未成单 1</span>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:10px">
<!-- 企业 1 · 已成单(可点击进入业务列表) -->
<div class="card" style="padding:14px 16px;border:1.5px solid transparent;position:relative">
<div style="position:absolute;top:0;right:0;background:#16A55A;color:#fff;font-size:10px;padding:3px 9px;border-radius:0 14px 0 8px;font-weight:600">已成单</div>
<div style="display:flex;align-items:flex-start;gap:10px;padding-right:50px">
<div style="width:32px;height:32px;background:#E5F7EC;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0">🏢</div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1A1A2E;line-height:1.4">北京卓思教育集团</div>
<div style="font-size:11px;color:#888;margin-top:3px">联系人 张先生 139****8877</div>
</div>
</div>
<div style="margin-top:10px;background:#F8F8FC;border-radius:8px;padding:8px 12px;display:flex;align-items:center;justify-content:space-between;font-size:11.5px;color:#666">
<span>邀请时间 <b style="color:#1A1A2E">2025-04-02</b></span>
<span>首单成交 <b style="color:#16A55A">2025-06-15</b></span>
</div>
</div>
<!-- 企业 2 · 已成单 -->
<div class="card" style="padding:14px 16px;border:1.5px solid transparent;position:relative">
<div style="position:absolute;top:0;right:0;background:#16A55A;color:#fff;font-size:10px;padding:3px 9px;border-radius:0 14px 0 8px;font-weight:600">已成单</div>
<div style="display:flex;align-items:flex-start;gap:10px;padding-right:50px">
<div style="width:32px;height:32px;background:#E5F7EC;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0">🏢</div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1A1A2E;line-height:1.4">深圳华创智联网络科技</div>
<div style="font-size:11px;color:#888;margin-top:3px">联系人 陈总 138****8800</div>
</div>
</div>
<div style="margin-top:10px;background:#F8F8FC;border-radius:8px;padding:8px 12px;display:flex;align-items:center;justify-content:space-between;font-size:11.5px;color:#666">
<span>邀请时间 <b style="color:#1A1A2E">2025-05-20</b></span>
<span>首单成交 <b style="color:#16A55A">2025-06-22</b></span>
</div>
</div>
<!-- 企业 3 · 未成单(不可跳转) -->
<div class="card" style="padding:14px 16px;position:relative;opacity:.85">
<div style="position:absolute;top:0;right:0;background:#E89B14;color:#fff;font-size:10px;padding:3px 9px;border-radius:0 14px 0 8px;font-weight:600">未成单</div>
<div style="display:flex;align-items:flex-start;gap:10px;padding-right:50px">
<div style="width:32px;height:32px;background:#FFF6E0;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0">🏢</div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1A1A2E;line-height:1.4">上海飞扬科技有限公司</div>
<div style="font-size:11px;color:#888;margin-top:3px">联系人 李女士 150****8888</div>
</div>
</div>
<div style="margin-top:10px;background:#F8F8FC;border-radius:8px;padding:8px 12px;display:flex;align-items:center;justify-content:space-between;font-size:11.5px;color:#666">
<span>邀请时间 <b style="color:#1A1A2E">2025-06-08</b></span>
<span style="color:#E89B14">暂未成单 · 已入驻18 天</span>
</div>
</div>
</div>
</div>
</div>
<div class="home"><div class="home-bar"></div></div>
</div>
</div>
<!-- ===== 关键逻辑注释 ===== -->
<style>
.logic-card { position:absolute; top:962px; width:375px; background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 4px 14px rgba(60,55,120,.08); border:1px solid rgba(22,165,90,.14); font-family:'Noto Sans SC',sans-serif; }
.logic-card .lc-title { display:flex; align-items:center; gap:8px; margin-bottom:10px; font-size:13px; font-weight:700; color:#1A1A2E; }
.logic-card .lc-dot { width:6px; height:6px; border-radius:50%; background:#16A55A; flex-shrink:0; }
.logic-card .lc-tag { font-size:10px; font-weight:600; padding:2px 7px; border-radius:5px; background:#E5F7EC; color:#16A55A; margin-left:auto; }
.logic-card ul { list-style:none; padding:0; margin:0; display:flex; flex-direction:column; gap:7px; }
.logic-card li { font-size:11.5px; line-height:1.55; color:#4A4866; padding-left:14px; position:relative; }
.logic-card li::before { content:''; position:absolute; left:2px; top:7px; width:4px; height:4px; border-radius:50%; background:#B5B3CD; }
.logic-card li b { color:#1A1A2E; font-weight:600; }
.logic-card .lc-sub { font-size:10.5px; color:#7775A0; margin-top:8px; padding-top:8px; border-top:1px dashed #EFEDF6; line-height:1.5; }
</style>
<div class="logic-card" style="left:80px">
<div class="lc-title"><span class="lc-dot"></span>P1 · 员工首页(介绍人)<span class="lc-tag">已登录 · 介绍人</span></div>
<ul>
<li><b>身份判定</b>:当前员工/普通用户邀请的合伙人,已有「企业客户产生订单」 → 显示为「介绍人」</li>
<li><b>未达介绍人条件</b>:仅完成邀请但未触发企业订单时,不显示介绍人徽章和业务板块,引导继续邀请</li>
<li><b>Hero 申请合伙人</b>:本人若尚未是合伙人,可点击「立即申请」 → 走 01 文件 P4 申请表单</li>
<li><b>邀请合伙人板块</b>:展示介绍合伙人数 / 推荐企业数 / 待领奖励;点击进入 P2 介绍人业务详情</li>
<li><b>内嵌邀请 banner</b>:「邀请朋友成为合伙人 ¥50/人」→ 唤起分享面板(专属海报 / 链接 / 微信)</li>
<li><b>主推/企服/小保</b>:浏览产品入口;员工点击「查看」进服务详情,介绍人不直接卖产品仅做拉新</li>
</ul>
<div class="lc-sub">介绍人 ≠ 对接人 ≠ 合伙人,三种身份相互独立;本视图为「内部员工 + 介绍人」共存视角</div>
</div>
<div class="logic-card" style="left:555px">
<div class="lc-title"><span class="lc-dot"></span>P2 · 介绍人业务详情<span class="lc-tag">业务详情</span></div>
<ul>
<li><b>奖励规则 banner</b>:明示「被邀请合伙人首单成交 → 本人得 ¥50 福礼金」;奖励由渠道管理员审核后线下发放</li>
<li><b>两宫格数据</b>:我介绍的合伙人(含已成单/未成单分布)+ 累计奖励(含已发放 vs 待发放)</li>
<li><b>我介绍的合伙人列表</b>:按加入时间倒序,卡片显示推荐企业数 / 已成单数 / 奖励状态(待发放 / 已发放 / 无奖励)</li>
<li><b>点击合伙人卡</b>:进入 P3 合伙人详情</li>
<li><b>奖励状态判定</b>:该合伙人下任意企业客户首单成交 → 待发放;渠道管理员发放后 → 已发放;无成单 → 无奖励</li>
</ul>
<div class="lc-sub">同一合伙人多次企业成单只触发一次 ¥50 奖励(首单即结算);后续企业订单走合伙人结算规则</div>
</div>
<div class="logic-card" style="left:1030px">
<div class="lc-title"><span class="lc-dot"></span>P3 · 合伙人详情<span class="lc-tag">合伙人画像</span></div>
<ul>
<li><b>头部卡</b>:合伙人基本信息 + 加入天数 + 双指标(推荐企业 / 已成单)</li>
<li><b>我获得的奖励</b>:展示从该合伙人获得的 ¥50 奖励金额及状态、首单触发企业、发放时间</li>
<li><b>推荐的企业列表</b>:状态分类 tab(全部 / 已成单 / 未成单),按邀请时间倒序</li>
<li><b>已成单企业卡</b>:展示邀请时间 + 首单成交时间;点击进入对应企业商机详情(介绍人有只读权限)</li>
<li><b>未成单企业卡</b>:展示邀请时间 + 已入驻天数;不展示业务订单(介绍人不关心订单细节)</li>
</ul>
<div class="lc-sub">介绍人视图仅展示「合伙人 → 企业」的转化漏斗;具体订单 / 佣金属于合伙人本人和对接人视角,不在此暴露</div>
</div>
</x-dc>
</body>
</html>
This diff could not be displayed because it is too large.
// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`.
// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`.
"use strict";
(() => {
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/react.ts
function getReact() {
const R = window.React;
if (!R) throw new Error("dc-runtime: window.React is not available yet");
return R;
}
function getReactDOM() {
const RD = window.ReactDOM;
if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet");
return RD;
}
var h = ((...args) => getReact().createElement(
...args
));
// src/parse.ts
function parseDcDocument(doc) {
const dc = doc.querySelector("x-dc");
if (!dc) return null;
const scriptEl = doc.querySelector("script[data-dc-script]");
const { props, preview } = parseDataProps(
scriptEl?.getAttribute("data-props") ?? null
);
return {
template: dc.innerHTML,
js: scriptEl ? scriptEl.textContent || "" : "",
props,
preview
};
}
function parseDcText(src) {
const openMatch = /<x-dc(?:\s[^>]*)?>/.exec(src);
if (!openMatch) return null;
const close = src.lastIndexOf("</x-dc>");
if (close === -1 || close < openMatch.index) return null;
const template = src.slice(openMatch.index + openMatch[0].length, close);
const doc = new DOMParser().parseFromString(src, "text/html");
const scriptEl = doc.querySelector("script[data-dc-script]");
const { props, preview } = parseDataProps(
scriptEl?.getAttribute("data-props") ?? null
);
return {
template,
js: scriptEl ? scriptEl.textContent || "" : "",
props,
preview
};
}
function parseDataProps(raw) {
if (!raw) return { props: null, preview: null };
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return { props: null, preview: null };
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return { props: null, preview: null };
}
const obj = parsed;
const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null;
const rest = {};
for (const k of Object.keys(obj)) {
if (k[0] !== "$") rest[k] = obj[k];
}
return { props: Object.keys(rest).length ? rest : null, preview };
}
function dcNameFromPath(pathname) {
let p = pathname || "";
try {
p = decodeURIComponent(p);
} catch {
}
const base = p.split("/").pop() || "Root";
return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root";
}
// src/boot.ts
var BASE_CSS = `
.sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent);
border:1px solid color-mix(in srgb,currentColor 50%,transparent);
border-radius:2px;box-sizing:border-box;overflow:hidden}
@keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}}
html.sc-dc-streaming .sc-placeholder,
html.sc-dc-streaming .sc-interp.sc-missing{position:relative;
background:color-mix(in srgb,currentColor 5%,transparent);
border-color:transparent}
html.sc-dc-streaming .sc-placeholder::before,
html.sc-dc-streaming .sc-interp.sc-missing::before{content:'';
position:absolute;inset:0;pointer-events:none;
background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%);
background-size:400% 100%;animation:sc-shine 1.4s ease infinite}
html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before,
html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none;
background:color-mix(in srgb,currentColor 8%,transparent)}
.sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace;
color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word}
.sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden;
vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5);
border-radius:2px;box-sizing:border-box;color:transparent;
user-select:none}
.sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em;
color:color-mix(in srgb,currentColor 50%,transparent);
background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px;
padding:0 3px}
.sc-host.sc-has-error{position:relative}
.sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch;
padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace;
border-radius:4px;white-space:pre-wrap;pointer-events:none}
/* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both
in sync until dc-runtime regains a build step. */
@media print {
@page { margin: 0.5cm; }
figure, table { break-inside: avoid; }
#dc-root, #dc-root > .sc-host { height: auto; }
*, *::before, *::after {
print-color-adjust: exact; -webkit-print-color-adjust: exact;
backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
animation-delay: -99s !important; animation-duration: .001s !important;
animation-iteration-count: 1 !important; animation-fill-mode: both !important;
animation-play-state: running !important; transition-duration: 0s !important;
}
}
`;
var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}";
function rootNameForDocument(doc, loc) {
let bootPath = loc.pathname || "";
if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) {
try {
bootPath = new URL(doc.baseURI || "/").pathname;
} catch {
}
}
return dcNameFromPath(bootPath);
}
function safeDecode(s) {
try {
return decodeURIComponent(s);
} catch {
return s;
}
}
function boot(runtime, doc = document) {
const parsed = parseDcDocument(doc);
if (!parsed) return null;
const React = getReact();
const rootName = rootNameForDocument(doc, location);
runtime.markFetched(rootName);
runtime.setRootName(rootName);
runtime.adoptParsed(rootName, parsed);
fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => {
const raw = t ? parseDcText(t) : null;
if (raw?.template) runtime.updateHtml(rootName, raw.template);
}).catch(() => {
});
const dc = doc.querySelector("x-dc");
const hostEl = doc.createElement("div");
hostEl.id = "dc-root";
dc.replaceWith(hostEl);
if (!parsed.preview) {
const s = doc.createElement("style");
s.textContent = FULL_PAGE_CSS;
doc.head.appendChild(s);
}
const Root = runtime.getDC(rootName);
const entry = runtime.registry.get(rootName);
function StandaloneRoot() {
const [, setTick] = React.useState(0);
React.useEffect(() => {
const sub = () => setTick((n) => n + 1);
entry.subs.add(sub);
return () => {
entry.subs.delete(sub);
};
}, []);
const defaults = React.useMemo(() => {
const d = {};
for (const k in entry.propsMeta || {}) {
const v = entry.propsMeta?.[k]?.default;
if (v !== void 0) d[k] = v;
}
return d;
}, [entry.propsMeta]);
return h(Root, { ...defaults, ...entry.propOverrides || {} });
}
const ReactDOM = getReactDOM();
if (ReactDOM.createRoot)
ReactDOM.createRoot(hostEl).render(h(StandaloneRoot));
else ReactDOM.render(h(StandaloneRoot), hostEl);
return rootName;
}
// src/expr.ts
var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/;
var NUMBER_RE = /^-?\d+(\.\d+)?$/;
function resolve(vals, src) {
const expr = String(src).trim();
if (!expr) return void 0;
if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) {
return resolve(vals, expr.slice(1, -1));
}
const eq = findTopLevelEquality(expr);
if (eq) {
const lv = resolve(vals, expr.slice(0, eq.index));
const rv = resolve(vals, expr.slice(eq.index + eq.op.length));
switch (eq.op) {
case "===":
return lv === rv;
case "!==":
return lv !== rv;
case "==":
return lv == rv;
default:
return lv != rv;
}
}
if (expr[0] === "!") return !resolve(vals, expr.slice(1));
if (expr === "true") return true;
if (expr === "false") return false;
if (expr === "null") return null;
if (expr === "undefined") return void 0;
if (NUMBER_RE.test(expr)) return Number(expr);
if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) {
return expr.slice(1, -1);
}
return resolvePath(vals, expr);
}
function parensWrapWhole(expr) {
let depth = 0;
for (let i = 0; i < expr.length - 1; i++) {
if (expr[i] === "(") depth++;
else if (expr[i] === ")") {
depth--;
if (depth === 0) return false;
}
}
return true;
}
function findTopLevelEquality(expr) {
let depth = 0;
for (let i = 0; i < expr.length; i++) {
const c = expr[i];
if (c === "[" || c === "(") depth++;
else if (c === "]" || c === ")") depth--;
else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") {
if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue;
if (!expr.slice(0, i).trim()) continue;
const op = expr[i + 2] === "=" ? c + "==" : c + "=";
return { index: i, op };
}
}
return null;
}
function resolvePath(vals, expr) {
const head = expr.match(IDENT_RE);
if (!head) return void 0;
let cur = vals == null ? void 0 : vals[head[0]];
let i = head[0].length;
while (i < expr.length) {
if (expr[i] === ".") {
const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/);
if (!m) return void 0;
cur = cur == null ? void 0 : cur[m[0]];
i += 1 + m[0].length;
} else if (expr[i] === "[") {
let depth = 1;
let j = i + 1;
while (j < expr.length && depth > 0) {
if (expr[j] === "[") depth++;
else if (expr[j] === "]") {
depth--;
if (depth === 0) break;
}
j++;
}
if (depth !== 0) return void 0;
const key = resolve(vals, expr.slice(i + 1, j));
cur = cur == null ? void 0 : cur[key];
i = j + 1;
} else {
return void 0;
}
}
return cur;
}
// src/encode.ts
var CAMEL_ATTR = "sc-camel-";
var INLINE_TEXT_TAGS = new Set(
"a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split(
" "
)
);
var RAW_WRAP = {
select: "sc-raw-select",
table: "sc-raw-table",
tbody: "sc-raw-tbody",
thead: "sc-raw-thead",
tfoot: "sc-raw-tfoot",
tr: "sc-raw-tr",
td: "sc-raw-td",
th: "sc-raw-th",
caption: "sc-raw-caption"
};
var RAW_UNWRAP = Object.fromEntries(
Object.entries(RAW_WRAP).map(([k, v]) => [v, k])
);
var EVENT_MAP = {
onclick: "onClick",
onchange: "onChange",
oninput: "onInput",
onsubmit: "onSubmit",
onkeydown: "onKeyDown",
onkeyup: "onKeyUp",
onkeypress: "onKeyPress",
onmousedown: "onMouseDown",
onmouseup: "onMouseUp",
onmouseenter: "onMouseEnter",
onmouseleave: "onMouseLeave",
onfocus: "onFocus",
onblur: "onBlur",
ondoubleclick: "onDoubleClick",
oncontextmenu: "onContextMenu"
};
var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`;
var IMPORT_SELF_CLOSE_RE = new RegExp(
"<(x-import|dc-import)(" + ATTRS + ")/>",
"gi"
);
var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g;
function encodeCase(html) {
html = html.replace(
IMPORT_SELF_CLOSE_RE,
(_, t, a) => "<" + t + a + "></" + t + ">"
);
html = html.replace(/<helmet(\s|>)/gi, "<sc-helmet$1");
html = html.replace(/<\/helmet\s*>/gi, "</sc-helmet>");
html = html.replace(
CAMEL_ATTR_RE,
(_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq
);
for (const [real, alias] of Object.entries(RAW_WRAP)) {
html = html.replace(
new RegExp("(</?)" + real + "(?=[\\s>])", "gi"),
"$1" + alias
);
}
return html;
}
function kebabToCamel(s) {
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
function cssToObj(css) {
const o = {};
for (const decl of css.split(";")) {
const i = decl.indexOf(":");
if (i < 0) continue;
const prop = decl.slice(0, i).trim();
o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim();
}
return o;
}
function compileAttr(raw) {
const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/);
if (whole) {
const path = whole[1];
return (vals) => resolve(vals, path);
}
if (raw.includes("{{")) {
const parts = raw.split(/\{\{([\s\S]+?)\}\}/g);
return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join("");
}
return () => raw;
}
// src/compile.ts
function collectProps(node, kind, host) {
const propGetters = [];
const pseudoClasses = [];
let hintSize = null;
for (const { name, value } of [...node.attributes]) {
if (name === "sc-name" || name === "data-dc-tpl") continue;
let key = name;
if (key.startsWith(CAMEL_ATTR))
key = kebabToCamel(key.slice(CAMEL_ATTR.length));
if (key === "hint-size") {
hintSize = value;
continue;
}
if (key.startsWith("style-")) {
pseudoClasses.push(host.pseudoClass(key.slice(6), value));
continue;
}
if (kind !== "dom") {
if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-"))))
key = kebabToCamel(key);
} else {
if (key === "class") key = "className";
else if (key === "for") key = "htmlFor";
else if (key.startsWith("on"))
key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3);
}
propGetters.push([key, compileAttr(value)]);
}
return { propGetters, pseudoClasses, hintSize };
}
var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([
"position",
"left",
"right",
"top",
"bottom",
"inset",
"width",
"height",
"z-index",
"transform"
]);
function hostPositionStyle(style) {
const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null;
if (!all) return void 0;
const out = {};
for (const [k, v] of Object.entries(all)) {
const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
if (HOST_STYLE_PROPS.has(kebab)) out[k] = v;
}
return Object.keys(out).length ? out : void 0;
}
function compileTemplate(html, host) {
const tpl = document.createElement("template");
//! nosemgrep: direct-inner-html-assignment
tpl.innerHTML = encodeCase(html);
let tplN = 0;
(function stamp(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
node.setAttribute("data-dc-tpl", String(tplN++));
}
for (const c of node.childNodes) stamp(c);
})(tpl.content);
const builders = walkChildren(tpl.content, host);
const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i)));
render.__annotated = tpl.innerHTML;
return render;
}
function walkChildren(node, host) {
return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null);
}
function walk(node, host) {
if (node.nodeType === Node.TEXT_NODE) return walkText(node);
if (node.nodeType !== Node.ELEMENT_NODE) return null;
const el = node;
const tag = el.tagName.toLowerCase();
if (tag === "sc-for") return walkFor(el, host);
if (tag === "sc-if") return walkIf(el, host);
if (tag === "x-import") return walkXImport(el, host);
if (tag === "sc-helmet") return host.helmet(el);
if (tag === "dc-import") return walkComponent(el, host);
return walkElement(el, host);
}
var warnedHoles = /* @__PURE__ */ new Set();
function warnUnresolved(ctx, what) {
const key = (ctx?.__name || "?") + "\0" + what;
if (warnedHoles.has(key)) return;
warnedHoles.add(key);
console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what);
}
function walkText(node) {
const txt = node.nodeValue ?? "";
if (!txt.includes("{{")) {
if (!txt.trim() && !txt.includes(" ")) return null;
return () => txt;
}
const parts = txt.split(/\{\{([\s\S]+?)\}\}/g);
return (vals, ctx, key) => h(
getReact().Fragment,
{ key },
...parts.map((p, i) => {
if (!(i & 1)) return p;
const v = resolve(vals, p);
if (v === void 0) {
if (!ctx?.__streamingNow) {
if (document.body?.hasAttribute("data-dc-editor-on")) {
return h(
"span",
{ key: i, className: "sc-interp sc-unresolved" },
"{{ " + p.trim() + " }}"
);
}
warnUnresolved(
ctx,
"{{ " + p.trim() + " }} never resolved \u2014 rendered as empty"
);
return null;
}
return h(
"span",
{ key: i, className: "sc-interp sc-missing" },
p.trim()
);
}
if (getReact().isValidElement(v) || Array.isArray(v)) {
return h(getReact().Fragment, { key: i }, v);
}
if (v === null || typeof v === "boolean") return null;
return h("span", { key: i, className: "sc-interp" }, String(v));
})
);
}
function walkFor(el, host) {
const listGet = compileAttr(el.getAttribute("list") || "");
const asName = el.getAttribute("as") || "item";
const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10);
const kids = walkChildren(el, host);
const listSrc = el.getAttribute("list") || "";
return (vals, ctx, key) => {
let list = listGet(vals);
if (!Array.isArray(list)) {
if (!ctx?.__streamingNow) {
if (list !== void 0 && list !== null) {
warnUnresolved(
ctx,
'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")"
);
}
list = [];
} else {
list = hintN > 0 ? Array(hintN).fill(void 0) : [];
}
}
return h(
getReact().Fragment,
{ key },
list.map((item, i) => {
const sub = { ...vals, [asName]: item, $index: i };
return h(
getReact().Fragment,
{ key: i },
kids.map((b, j) => b(sub, ctx, j))
);
})
);
};
}
function walkIf(el, host) {
const valGet = compileAttr(el.getAttribute("value") || "");
const hintRaw = el.getAttribute("hint-placeholder-val");
const hintGet = hintRaw != null ? compileAttr(hintRaw) : null;
const kids = walkChildren(el, host);
return (vals, ctx, key) => {
let v = valGet(vals);
if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals);
return v ? h(
getReact().Fragment,
{ key },
kids.map((b, j) => b(vals, ctx, j))
) : null;
};
}
function walkComponent(el, host) {
const name = el.getAttribute("name") || el.getAttribute("component") || "";
el.removeAttribute("name");
el.removeAttribute("component");
const tplId = el.getAttribute("data-dc-tpl");
const styleRaw = el.getAttribute("style");
el.removeAttribute("style");
const styleGet = styleRaw != null ? compileAttr(styleRaw) : null;
const { propGetters, hintSize } = collectProps(el, "dc-import", host);
const kids = walkChildren(el, host);
return (vals, ctx, key) => {
const props = {
key,
__hintSize: hintSize,
__tplId: tplId,
__hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0
};
for (const [k, g] of propGetters) {
const v = g(vals);
if (k === "dcProps") {
if (v && typeof v === "object") Object.assign(props, v);
continue;
}
props[k] = v;
}
if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j));
return h(host.component(name), props);
};
}
function walkXImport(el, host) {
const globalNameGet = compileAttr(
el.getAttribute("component-from-global-scope") || ""
);
const exportNameGet = compileAttr(
el.getAttribute("component") || el.getAttribute("name") || ""
);
const fromRaw = el.getAttribute("from") || el.getAttribute("src") || el.getAttribute("import") || "";
const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : [];
const url = urls.length ? urls[urls.length - 1] : "";
const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js";
const tplId = el.getAttribute("data-dc-tpl");
const styleRaw = el.getAttribute("style");
el.removeAttribute("style");
const styleGet = styleRaw != null ? compileAttr(styleRaw) : null;
const wrap = tplId != null || styleGet != null;
const { propGetters, hintSize } = collectProps(el, "x-import", host);
const hasContent = el.children.length > 0 || !!(el.textContent || "").trim();
const kids = hasContent ? walkChildren(el, host) : [];
const urlBindable = fromRaw.includes("{{");
if (urls.length && !urlBindable) {
let prev;
for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev);
}
const evalName = (g, vals) => {
const v = g(vals);
const s = v == null ? "" : String(v);
return s.includes("{{") ? "" : s;
};
return (vals, ctx, key) => {
const globalName = evalName(globalNameGet, vals);
const name = globalName || evalName(exportNameGet, vals);
const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name);
const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0;
const wrapper = wrap ? {
key,
className: "sc-host-x",
"data-dc-tpl": tplId,
style: hostStyle || { display: "contents" }
} : null;
if (!C) {
const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name);
const ph = host.placeholder({
key: wrapper ? void 0 : key,
name,
hintSize,
error
});
return wrapper ? h("div", wrapper, ph) : ph;
}
const props = wrapper ? {} : { key };
let unresolvedHole = false;
for (const [k, g] of propGetters) {
if (k === "component" || k === "componentFromGlobalScope" || k === "from") {
continue;
}
const v = g(vals);
if (v === void 0) unresolvedHole = true;
if (k === "dcProps") {
if (v && typeof v === "object") Object.assign(props, v);
continue;
}
props[k] = v;
}
if (unresolvedHole && ctx?.__htmlStreamingNow) {
const ph = host.placeholder({
key: wrapper ? void 0 : key,
name,
hintSize,
error: null
});
return wrapper ? h("div", wrapper, ph) : ph;
}
if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j));
return wrapper ? h("div", wrapper, h(C, props)) : h(C, props);
};
}
function contentKey(el) {
const clone = el.cloneNode(true);
for (const d of clone.querySelectorAll("*")) {
while (d.attributes.length) d.removeAttribute(d.attributes[0].name);
}
const s = clone.innerHTML;
let h2 = 5381;
for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0;
return s.length + "." + (h2 >>> 0).toString(36);
}
var NEVER_CONTENT_KEYED = new Set(
"script style textarea option title select canvas iframe video audio".split(
" "
)
);
var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")";
function walkElement(el, host) {
const realTag = RAW_UNWRAP[el.localName] || el.localName;
const tplId = el.getAttribute("data-dc-tpl");
const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null;
const keySuffix = inlineOnly ? "|" + contentKey(el) : "";
const { propGetters, pseudoClasses } = collectProps(el, "dom", host);
const kids = walkChildren(el, host);
return (vals, ctx, key) => {
const props = {
key: key + keySuffix,
"data-dc-tpl": tplId
};
for (const [k, g] of propGetters) {
let v = g(vals);
if (k === "style" && typeof v === "string") v = cssToObj(v);
if ((k === "value" || k === "checked") && v === void 0) {
v = k === "checked" ? false : "";
}
props[k] = v;
}
if (pseudoClasses.length) {
props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" ");
}
return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j)));
};
}
// src/logic.ts
var StreamableLogic = class {
constructor(props) {
__publicField(this, "props");
__publicField(this, "state", {});
/** Back-pointer to the wrapper component, installed after construction. */
__publicField(this, "__host");
this.props = props || {};
}
setState(update, cb) {
this.__host && this.__host.__setLogicState(update, cb);
}
forceUpdate() {
this.__host && this.__host.forceUpdate();
}
componentDidMount() {
}
componentDidUpdate(_prevProps) {
}
componentWillUnmount() {
}
/** The flat object the template renders against (merged over props). */
renderVals() {
return {};
}
};
function evalDcLogic(src) {
//! nosemgrep: eval-and-function-constructor
const fn = new Function(
"DCLogic",
"StreamableLogic",
"React",
src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;'
);
return fn(StreamableLogic, StreamableLogic, getReact());
}
// src/component.ts
function shallowEqual(a, b) {
if (!b) return false;
const ak = Object.keys(a).filter((k) => k !== "children");
const bk = Object.keys(b).filter((k) => k !== "children");
if (ak.length !== bk.length) return false;
for (const k of ak) if (a[k] !== b[k]) return false;
return true;
}
function Placeholder({
name,
hintSize,
streaming,
error
}) {
const [w, hgt] = (hintSize || "100%,60px").split(",");
return h(
"div",
{
className: "sc-placeholder" + (streaming ? " sc-streaming" : ""),
style: { width: w.trim(), height: hgt && hgt.trim() },
title: name
},
error ? h(
"div",
{ className: "sc-placeholder-error" },
(name ? name + ": " : "") + error
) : null
);
}
function hintToMin(hint) {
if (!hint) return void 0;
const [w, hgt] = hint.split(",");
return { minWidth: w.trim(), minHeight: hgt && hgt.trim() };
}
function createComponentFactory(registry, ensureFetched) {
const React = getReact();
const AncestorContext = React.createContext([]);
class StreamableComponent extends React.Component {
constructor(props) {
super(props);
__publicField(this, "__name");
__publicField(this, "__sub");
__publicField(this, "__needsDidMount", false);
/** Snapshot of the registry's streaming flags taken at render time —
* builders read it off the RenderCtx (this) to pick placeholder vs
* render-nothing for unresolved values. */
__publicField(this, "__streamingNow", false);
__publicField(this, "__htmlStreamingNow", false);
/** When a construct throws, remember the (class, registry.ver, props)
* triple so render-time reconcile doesn't re-attempt it on every parent
* re-render. A registry bump (new class, template, external module
* resolving via bumpAll) changes `ver` and breaks the memo so an
* env-dependent constructor can self-heal. */
__publicField(this, "__failedLogic", null);
__publicField(this, "__failedUserProps", null);
__publicField(this, "__failedVer", -1);
/** Per-instance constructor error — kept here (not on the registry entry)
* so one instance's successful construct can't hide a sibling's failure,
* and a construct can never wipe an eval error `updateJs` recorded on
* `r.logicError`. */
__publicField(this, "__ctorError", null);
__publicField(this, "logic");
this.__name = props.__name;
this.state = { __v: 0, __err: null };
this.__sub = () => {
if (this.state.__err) this.setState({ __err: null });
this.forceUpdate();
};
this.__makeLogic(registry.get(this.__name).Logic, null);
ensureFetched(this.__name);
}
/** Error-boundary hook: a render crash anywhere in this DC's subtree
* (its own template, an x-import'd component, a child DC without its
* own deeper boundary) lands here instead of unmounting the page. */
static getDerivedStateFromError(e) {
return { __err: e instanceof Error && e.message ? e.message : String(e) };
}
componentDidCatch(e, info) {
console.error(
"[dc-runtime] render error in <" + this.__name + ">:",
e,
info?.componentStack || ""
);
}
/** Instantiate the logic class (or the no-op base) and adopt `prevState`
* over its initial state — used both at mount and on hot-swap. */
__makeLogic(Logic, prevState) {
const L = Logic || StreamableLogic;
try {
this.logic = new L(this.__userProps());
this.__failedLogic = null;
this.__failedUserProps = null;
this.__ctorError = null;
} catch (e) {
console.error(e);
this.__failedLogic = Logic;
this.__failedUserProps = this.__userProps();
this.__failedVer = registry.get(this.__name).ver;
this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e));
this.logic = new StreamableLogic(
this.__userProps()
);
}
this.logic.__host = this;
if (prevState)
this.logic.state = { ...this.logic.state || {}, ...prevState };
}
/** The props the author's logic + template see — internal __-prefixed
* wiring stripped. */
__userProps() {
const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props;
return rest;
}
__setLogicState(update, cb) {
const prev = this.logic.state;
const patch = typeof update === "function" ? update(prev) : update;
this.logic.state = { ...prev, ...patch };
this.setState((s) => ({ __v: s.__v + 1 }), cb);
}
/** Swap the logic instance when the registry's Logic class changed
* (streaming completion, hot reload). State carries over; didMount
* re-fires after the swap commits so refs exist. */
__reconcileLogic() {
const r = registry.get(this.__name);
const Next = r.Logic;
const Cur = this.logic.constructor;
if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) {
return;
}
if (!this.__needsDidMount) {
try {
this.logic.componentWillUnmount();
} catch (e) {
console.error(e);
}
}
this.__makeLogic(Next, this.logic.state);
this.__needsDidMount = true;
}
componentDidMount() {
registry.get(this.__name).subs.add(this.__sub);
try {
this.logic.componentDidMount();
} catch (e) {
console.error(e);
}
}
componentDidUpdate(prevProps) {
this.logic.props = this.__userProps();
if (this.__needsDidMount) {
if (this.state.__err || !registry.get(this.__name).tpl) return;
this.__needsDidMount = false;
try {
this.logic.componentDidMount();
} catch (e) {
console.error(e);
}
} else {
try {
this.logic.componentDidUpdate(prevProps);
} catch (e) {
console.error(e);
}
}
}
componentWillUnmount() {
registry.get(this.__name).subs.delete(this.__sub);
if (!this.__needsDidMount) {
try {
this.logic.componentWillUnmount();
} catch (e) {
console.error(e);
}
}
}
render() {
const r = registry.get(this.__name);
const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : "");
const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0;
const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0;
const hostBase = {
className: cls,
style: hostStyle,
"data-sc-name": this.__name,
"data-dc-tpl": this.props.__tplId
};
const chain = Array.isArray(this.context) ? this.context : [];
if (chain.includes(this.__name)) {
const cycle = [
...chain.slice(chain.indexOf(this.__name)),
this.__name
].join(" \u2192 ");
return h(
"div",
{ ...hostBase, className: cls + " sc-has-error" },
h(Placeholder, {
name: this.__name,
hintSize: this.props.__hintSize,
error: "circular import: " + cycle
})
);
}
if (this.state.__err) {
return h(
"div",
{ ...hostBase, className: cls + " sc-has-error" },
h(
"div",
{ className: "sc-logic-error", "data-omelette-chrome": "" },
this.__name + ": " + this.state.__err
),
h(Placeholder, {
name: this.__name,
hintSize: this.props.__hintSize,
error: this.state.__err
})
);
}
this.__reconcileLogic();
if (!r.tpl) {
return h(
"div",
hostBase,
h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize })
);
}
const userProps = this.__userProps();
this.logic.props = userProps;
let vals = userProps;
let renderErr = r.logicError || this.__ctorError;
try {
vals = { ...userProps, ...this.logic.renderVals() || {} };
} catch (e) {
console.error(e);
renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e));
}
this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming);
this.__htmlStreamingNow = !!r.htmlStreaming;
return h(
"div",
{ ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") },
renderErr && h(
"div",
{ className: "sc-logic-error", "data-omelette-chrome": "" },
renderErr
),
h(
AncestorContext.Provider,
{ value: [...chain, this.__name] },
r.tpl(vals, this)
)
);
}
}
__publicField(StreamableComponent, "contextType", AncestorContext);
const named = /* @__PURE__ */ new Map();
function getDC(name) {
const hit = named.get(name);
if (hit) return hit;
function Dispatcher(p) {
const [, setTick] = React.useState(0);
React.useEffect(() => {
const sub = () => setTick((n) => n + 1);
registry.get(name).subs.add(sub);
return () => {
registry.get(name).subs.delete(sub);
};
}, []);
ensureFetched(name);
return h(StreamableComponent, { ...p, __name: name });
}
Dispatcher.displayName = name;
named.set(name, Dispatcher);
return Dispatcher;
}
return {
getDC,
StreamableComponent
};
}
// src/external.ts
var isCustomElementName = (n) => !n.includes(".") && n.includes("-");
function isRenderableType(g) {
if (typeof g === "function") return !isElementClass(g);
return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol";
}
function resolveDottedPath(root, name) {
let cur = root;
for (const seg of name.split(".")) {
if (cur == null) return void 0;
cur = cur[seg];
}
return cur;
}
var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js";
var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y";
var GLOBAL_POLL_INTERVAL_MS = 50;
var GLOBAL_POLL_TIMEOUT_MS = 3e4;
function createExternalModules(onResolved) {
const cache = /* @__PURE__ */ new Map();
let babelLoading = null;
const reportedMissing = /* @__PURE__ */ new Map();
const polling = /* @__PURE__ */ new Set();
function ensureBabel() {
if (window.Babel) return Promise.resolve();
if (babelLoading) return babelLoading;
babelLoading = new Promise((res, rej) => {
const s = document.createElement("script");
s.src = BABEL_URL;
s.integrity = BABEL_SRI;
s.crossOrigin = "anonymous";
s.onload = () => res();
s.onerror = rej;
document.head.appendChild(s);
});
return babelLoading;
}
const pending = /* @__PURE__ */ new Map();
function load(kind, url, after) {
const existing = pending.get(url);
if (existing) return existing;
cache.set(url, null);
console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")");
const ready = Promise.all([
kind === "jsx" ? ensureBabel() : Promise.resolve(),
after ?? Promise.resolve()
]);
const p = ready.then(() => fetch(url)).then((r) => {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.text();
}).then((src) => {
const code = kind === "jsx" ? window.Babel.transform(src, {
filename: url,
presets: ["react", "typescript"]
}).code : src;
const module = { exports: {} };
const before = new Set(Object.keys(window));
//! nosemgrep: eval-and-function-constructor
new Function("React", "module", "exports", "require", code)(
getReact(),
module,
module.exports,
() => ({})
);
const globals = {};
for (const k of Object.keys(window)) {
if (!before.has(k) && typeof window[k] === "function") {
globals[k] = window[k];
}
}
cache.set(url, { mod: module.exports, globals });
console.info(
"[dc-runtime] x-import: loaded",
url,
"\u2014 exports:",
Object.keys(module.exports),
"window globals:",
Object.keys(globals)
);
onResolved();
}).catch((e) => {
cache.set(url, {
mod: {},
globals: {},
error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e))
});
console.error(
"[dc-runtime] x-import: FAILED to load",
url,
"(" + kind + ")",
e
);
onResolved();
});
pending.set(url, p);
return p;
}
function resolve2(url, name) {
const entry = cache.get(url);
if (!entry) return null;
const { mod, globals } = entry;
const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default;
if (typeof C === "function") return C;
const key = url + "\0" + name;
if (!reportedMissing.has(key)) {
reportedMissing.set(
key,
entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")"
);
console.error(
"[dc-runtime] x-import: module",
url,
"loaded but has no component named",
JSON.stringify(name),
"\u2014 available exports:",
Object.keys(mod),
"window globals:",
Object.keys(globals),
". The module must `module.exports = {" + name + "}` or set `window." + name + "`."
);
}
return null;
}
function waitForGlobal(name) {
if (polling.has(name)) return;
polling.add(name);
const started = Date.now();
const isCE = isCustomElementName(name);
const tick = () => {
const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name));
if (found) {
polling.delete(name);
onResolved();
return;
}
if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) {
console.warn(
"[dc-runtime] x-import: global",
JSON.stringify(name),
"never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms"
);
return;
}
setTimeout(tick, GLOBAL_POLL_INTERVAL_MS);
};
setTimeout(tick, GLOBAL_POLL_INTERVAL_MS);
}
function resolveGlobal(url, name) {
const isCE = isCustomElementName(name);
if (!url) {
if (isCE) {
if (customElements.get(name)) return name;
waitForGlobal(name);
return null;
}
const g2 = resolveDottedPath(window, name);
if (isRenderableType(g2)) return g2;
waitForGlobal(name);
return null;
}
const entry = cache.get(url);
if (!entry) return null;
if (isCE && customElements.get(name)) return name;
const g = entry.globals[name] ?? resolveDottedPath(window, name);
if (isRenderableType(g)) return g;
if (name.includes(".")) return null;
const key = url + "\0global\0" + name;
if (!reportedMissing.has(key)) {
reportedMissing.set(key, null);
if (isCE && !customElements.get(name)) {
console.warn(
"[dc-runtime] x-import:",
url,
"loaded but no custom element",
JSON.stringify(name),
"is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element."
);
}
}
return name;
}
function getError(url, name) {
const entry = cache.get(url);
if (entry?.error) return entry.error;
return reportedMissing.get(url + "\0" + name) || null;
}
return { load, resolve: resolve2, resolveGlobal, getError };
}
function isElementClass(g) {
try {
return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement;
} catch {
return false;
}
}
// src/atomics.ts
var ATOMIC_CSS = (
// layout
".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}"
);
// src/helmet.ts
var DESIGN_DOC_MODE_RE = /<meta\b[^>]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i;
var CANVAS_BG_LIGHT = "#f0eee6";
var CANVAS_BG_DARK = "#2e2c26";
function createHelmetManager(doc, isStreaming) {
const mounted = /* @__PURE__ */ new Set();
const live = /* @__PURE__ */ new Map();
let designDocMode = null;
let canvasStyleEl = null;
let appTheme = "light";
try {
const ds = doc.documentElement.dataset.theme;
appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get(
"theme"
) === "dark" ? "dark" : "light";
} catch {
}
function applyCanvasBg() {
if (!canvasStyleEl) return;
const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT;
canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`;
}
function postDesignMode(mode) {
if (window.parent === window) return;
try {
window.parent.postMessage({ type: "__dc_design_mode", mode }, "*");
} catch {
}
}
function setDesignDocMode(mode) {
if (mode === designDocMode) return;
designDocMode = mode;
postDesignMode(mode);
if (mode === "canvas") {
doc.documentElement.setAttribute("data-dc-canvas", "");
canvasStyleEl = doc.createElement("style");
canvasStyleEl.setAttribute("data-dc-canvas", "");
applyCanvasBg();
doc.head.appendChild(canvasStyleEl);
} else {
doc.documentElement.removeAttribute("data-dc-canvas");
canvasStyleEl?.remove();
canvasStyleEl = null;
}
}
window.addEventListener("message", (e) => {
const type = e.data && e.data.type;
if (type === "__dc_theme") {
const t = e.data.theme;
if (t === "light" || t === "dark") {
appTheme = t;
doc.documentElement.dataset.theme = t;
applyCanvasBg();
}
return;
}
if (!designDocMode || type !== "__dc_probe") return;
postDesignMode(designDocMode);
});
function compile(node) {
const raw = [...node.children];
const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null;
if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) {
mounted.add("__dc-atomics");
const el = doc.createElement("style");
el.id = "__dc-atomics";
el.textContent = ATOMIC_CSS;
doc.head.appendChild(el);
}
return (_vals, ctx) => {
const name = ctx && ctx.__name || "";
const streaming = !!(name && isStreaming(name));
for (let i = 0; i < raw.length; i++) {
const child = raw[i];
const tag = child.tagName;
const mayBePartial = streaming && !helmetClosed && i === raw.length - 1;
if (tag === "SCRIPT") {
if (mayBePartial) continue;
const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || "");
if (mounted.has(key)) continue;
mounted.add(key);
const el = doc.createElement("script");
for (const { name: an, value } of [...child.attributes])
el.setAttribute(an, value);
if (child.textContent) el.textContent = child.textContent;
doc.head.appendChild(el);
} else if (tag === "LINK" || tag === "META") {
if (mayBePartial) continue;
const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML);
if (mounted.has(key)) continue;
mounted.add(key);
doc.head.appendChild(child.cloneNode(true));
} else {
const key = name + "|" + i;
let el = live.get(key);
if (!el || el.tagName !== tag) {
if (el) el.remove();
el = doc.createElement(tag.toLowerCase());
live.set(key, el);
doc.head.appendChild(el);
}
for (const { name: an, value } of [...child.attributes]) {
if (el.getAttribute(an) !== value) el.setAttribute(an, value);
}
if (el.textContent !== child.textContent)
el.textContent = child.textContent;
}
}
return null;
};
}
return { compile, setDesignDocMode };
}
// src/pseudo.ts
function createPseudoSheet(doc) {
let el = null;
const cache = /* @__PURE__ */ new Map();
let n = 0;
return (pseudo, css) => {
const k = pseudo + "|" + css;
const hit = cache.get(k);
if (hit) return hit;
if (!el) {
el = doc.createElement("style");
doc.head.appendChild(el);
}
const cls = "scp" + (n++).toString(36);
const sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo;
el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length);
cache.set(k, cls);
return cls;
};
}
// src/registry.ts
function createRegistry() {
const entries = /* @__PURE__ */ Object.create(null);
function get(name) {
return entries[name] || (entries[name] = {
html: "",
tpl: null,
Logic: null,
jsStreaming: false,
htmlStreaming: false,
ver: 0,
subs: /* @__PURE__ */ new Set(),
fetched: false
});
}
function bump(name) {
const r = get(name);
r.ver++;
for (const fn of r.subs) fn();
}
return {
entries,
get,
bump,
bumpAll() {
for (const n in entries) bump(n);
}
};
}
// src/runtime.ts
var COMPONENT_DIR = ".";
function createRuntime(doc = document) {
const registry = createRegistry();
const pseudoClass = createPseudoSheet(doc);
const helmet = createHelmetManager(
doc,
(name) => registry.get(name).htmlStreaming
);
const external = createExternalModules(() => registry.bumpAll());
const factory = createComponentFactory(registry, ensureFetched);
const host = {
component: (name) => factory.getDC(name),
placeholder: (props) => h(Placeholder, props),
helmet: (node) => helmet.compile(node),
loadExternal: (kind, url, after) => external.load(kind, url, after),
resolveExternal: (url, name) => external.resolve(url, name),
resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name),
resolveExternalError: (url, name) => external.getError(url, name),
pseudoClass
};
function ensureFetched(name) {
const r = registry.get(name);
if (r.fetched) return;
r.fetched = true;
const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html";
fetch(url).then((res) => {
if (!res.ok) {
console.error(
"[dc-runtime] sibling fetch for <" + name + "/> failed:",
url,
"returned",
res.status,
"\u2014 the reference renders as an empty placeholder."
);
return "";
}
return res.text();
}).then((t) => {
if (!t) return;
const parsed = parseDcText(t);
if (!parsed) {
console.error(
"[dc-runtime] sibling fetch for <" + name + "/>:",
url,
"has no <x-dc> block \u2014 not a Design Component."
);
return;
}
if (parsed.props) r.propsMeta = parsed.props;
if (parsed.preview) r.preview = parsed.preview;
if (parsed.template && !r.html) updateHtml(name, parsed.template);
if (parsed.js && !r.Logic) updateJs(name, parsed.js);
}).catch(
(e) => console.error(
"[dc-runtime] sibling fetch for <" + name + "/> threw:",
url,
e
)
);
}
let rootName = null;
function updateHtml(name, html) {
const r = registry.get(name);
r.html = html;
if (name === rootName) {
const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null;
if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode);
}
try {
r.tpl = compileTemplate(html, host);
} catch (e) {
console.error("[dc-runtime] template compile FAILED for", name, e);
}
registry.bump(name);
}
function updateJs(name, src) {
const r = registry.get(name);
const seq = r.jsSeq = (r.jsSeq || 0) + 1;
try {
const Cls = evalDcLogic(src);
if (r.jsSeq !== seq) return;
if (typeof Cls !== "function") {
r.logicError = name + ".dc.html: <script data-dc-script> must define `class Component extends DCLogic`";
} else {
r.logicError = null;
r.Logic = Cls;
}
} catch (e) {
if (r.jsSeq !== seq) return;
console.error(
"[dc-runtime] logic class eval FAILED for",
name,
"\u2014 the template renders with props only.",
e
);
r.logicError = name + ": " + (e instanceof Error && e.message ? e.message : String(e));
}
registry.bump(name);
}
function setStreaming(name, kind, on) {
const r = registry.get(name);
if (kind === "html") r.htmlStreaming = !!on;
else r.jsStreaming = !!on;
let any = false;
for (const n in registry.entries) {
const e = registry.entries[n];
if (e && (e.htmlStreaming || e.jsStreaming)) {
any = true;
break;
}
}
doc.documentElement.classList.toggle("sc-dc-streaming", any);
registry.bump(name);
}
function dcUpdate(name, kind, content, streaming) {
if (streaming) registry.get(name).fetched = true;
if (kind === "html") {
setStreaming(name, "html", !!streaming);
updateHtml(name, content);
} else if (kind === "js") {
setStreaming(name, "js", !!streaming);
if (!streaming) updateJs(name, content);
} else if (kind === "props") {
const { props, preview } = parseDataProps(content);
const r = registry.get(name);
r.propsMeta = props ?? void 0;
r.preview = preview;
registry.bump(name);
}
}
function setProps(name, overrides) {
registry.get(name).propOverrides = overrides && typeof overrides === "object" ? { ...overrides } : null;
registry.bump(name);
}
function adoptParsed(name, parsed) {
if (!parsed) return;
const r = registry.get(name);
if (parsed.props) r.propsMeta = parsed.props;
if (parsed.preview) r.preview = parsed.preview;
if (parsed.template) updateHtml(name, parsed.template);
if (parsed.js) updateJs(name, parsed.js);
}
return {
registry,
getDC: factory.getDC,
updateHtml,
updateJs,
dcUpdate,
setProps,
adoptParsed,
setRootName: (name) => {
rootName = name;
},
markFetched: (name) => {
registry.get(name).fetched = true;
},
annotatedTemplate: (name) => {
const r = registry.get(name);
return r.tpl && r.tpl.__annotated || null;
},
templateSource: (name) => registry.get(name).html || null,
StreamableLogic
};
}
// src/index.ts
var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js";
var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z";
var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js";
var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1";
function hideRawTemplate() {
const s = document.createElement("style");
s.textContent = "x-dc{display:none!important}";
document.head.appendChild(s);
}
function loadScript(src, integrity) {
return new Promise((resolve2, reject) => {
//! nosemgrep: create-script-element
const s = document.createElement("script");
s.src = src;
s.integrity = integrity;
s.crossOrigin = "anonymous";
s.async = false;
s.onload = () => resolve2();
s.onerror = () => reject(new Error(`failed to load ${src}`));
document.head.appendChild(s);
});
}
function loadReactUmd() {
const w = window;
if (w.React && w.ReactDOM) return Promise.resolve();
return Promise.all([
loadScript(REACT_URL, REACT_SRI),
loadScript(REACT_DOM_URL, REACT_DOM_SRI)
]).then(() => void 0);
}
function init() {
const runtime = createRuntime(document);
let rootName = "Root";
const baseCss = document.createElement("style");
baseCss.textContent = BASE_CSS;
document.head.prepend(baseCss);
const notifyHost = () => {
if (window.parent === window) return;
const r = runtime.registry.entries[rootName];
try {
window.parent.postMessage(
{
type: "__dc_booted",
rootName,
propsMeta: r && r.propsMeta || null,
preview: r && r.preview || null
},
"*"
);
} catch {
}
};
const api = {
__dcUpdate: (name, kind, content, streaming) => {
runtime.dcUpdate(name, kind, content, streaming);
if (name === rootName && !streaming && kind === "props") notifyHost();
},
__dcSetProps: (name, overrides) => runtime.setProps(name, overrides),
/** Name of the component currently mounted as the page root — DC tools
* push their template-stream here when targeting "the open page". */
__dcRootName: () => rootName,
/** Editor bridge — the encoded, `data-dc-tpl`-annotated template source.
* The host editor parses this into its own template DOM so it can map a
* rendered node (carrying the same `data-dc-tpl`) back to the source
* node that emitted it. Returns the encoded form (`<sc-comp>`,
* `sc-camel-*` attrs); the editor decodes on serialize. */
__dcAnnotatedTemplate: (name) => runtime.annotatedTemplate(name),
/** Editor bridge — the *original* (decoded) template source. */
__dcTemplateSource: (name) => runtime.templateSource(name),
__dcBoot: () => {
rootName = boot(runtime, document) ?? rootName;
notifyHost();
},
__dcRegistry: runtime.registry.entries,
getDC: (name) => runtime.getDC(name),
// `DCLogic` is the documented base class name; `StreamableLogic` is the
// implementation alias kept for any project that already references it.
DCLogic: runtime.StreamableLogic,
StreamableLogic: runtime.StreamableLogic
};
Object.assign(window, api);
window.__dcContentKeyed = true;
if (document.readyState !== "loading") api.__dcBoot();
else document.addEventListener("DOMContentLoaded", () => api.__dcBoot());
}
hideRawTemplate();
loadReactUmd().then(init).catch((err) => {
console.error("[dc] failed to load React or boot:", err);
throw err;
});
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment