chore: 提交调试文档与模拟器改动
This commit is contained in:
333
doc/MyToDo.md
Normal file
333
doc/MyToDo.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
|
||||||
|
结果页会根据客户的要求不停的变换,用什么方案能实现这个需求,其实其他的弹出内容也都存在这个问题,样式,内容都时根据客户需求变化的,怎样一种方案设计比较好呢?
|
||||||
|
|
||||||
|
|
||||||
|
我想改造下GPS模拟器,做成一个数据中转程序,这样后面的开发模拟,家长端监控,场控,数据回放就都能支持了,类似于路由器,只中转不保存数据,你觉得可行否?或者还有没有更好的方案?
|
||||||
|
技术栈要轻量,健壮,性能第一,类似软路由这类应用
|
||||||
|
|
||||||
|
|
||||||
|
目前已经把卡片架构的底座搭出来了,进度可以概括成:
|
||||||
|
|
||||||
|
已经完成的
|
||||||
|
1. 原生内容卡主链已经成立
|
||||||
|
现在控制点内容不再是单一硬编码弹层,而是已经支持:
|
||||||
|
|
||||||
|
title / body
|
||||||
|
clickTitle / clickBody
|
||||||
|
autoPopup
|
||||||
|
once
|
||||||
|
priority
|
||||||
|
template
|
||||||
|
而且:
|
||||||
|
|
||||||
|
start-1 / control-N / finish-1 都可配置
|
||||||
|
起终点同位置时,点击内容优先级也已经处理了
|
||||||
|
2. 原生内容卡模板化已经落地
|
||||||
|
目前已经有 3 种原生模板:
|
||||||
|
|
||||||
|
minimal
|
||||||
|
story
|
||||||
|
focus
|
||||||
|
默认策略也定了:
|
||||||
|
|
||||||
|
起点/终点:focus
|
||||||
|
普通点:story
|
||||||
|
3. 内容展示机制已经不是“弹了就完”
|
||||||
|
现在已经有:
|
||||||
|
|
||||||
|
自动弹
|
||||||
|
手动查看入口
|
||||||
|
队列
|
||||||
|
优先级覆盖
|
||||||
|
once 本局只展示一次
|
||||||
|
也就是说,已经开始像正式的“内容体验层”了。
|
||||||
|
|
||||||
|
4. H5 分工已经定案并接进去了
|
||||||
|
这条边界已经试清楚了:
|
||||||
|
|
||||||
|
即时内容弹窗:原生
|
||||||
|
H5:详情页 / 互动任务页 / 结果页
|
||||||
|
H5 不再直接顶替原生弹窗
|
||||||
|
当前实际链路是:
|
||||||
|
|
||||||
|
原生内容卡 -> 查看详情 CTA -> H5 详情页
|
||||||
|
|
||||||
|
5. H5 容器页已经能用了
|
||||||
|
企业主体切过来后:
|
||||||
|
|
||||||
|
web-view 已经能打开
|
||||||
|
最小测试链路已经通了
|
||||||
|
但也验证了一个结论:
|
||||||
|
小程序 web-view 不适合做局部弹窗内容区
|
||||||
|
适合整页详情/任务/结果页
|
||||||
|
现在还没完全做完的
|
||||||
|
1. 原生内容卡还不够“组件化”
|
||||||
|
现在模板化已经有了,但还没完全形成更正式的:
|
||||||
|
|
||||||
|
卡片 ViewModel
|
||||||
|
卡片模板注册表
|
||||||
|
模板渲染规范
|
||||||
|
现在还是“已可用,但还不够体系化”。
|
||||||
|
|
||||||
|
2. CTA 还比较初级
|
||||||
|
现在 查看详情 能用了,但还缺:
|
||||||
|
|
||||||
|
CTA 文案配置化
|
||||||
|
CTA 图标 / 样式分级
|
||||||
|
多动作支持
|
||||||
|
查看详情
|
||||||
|
开始互动
|
||||||
|
拍照任务
|
||||||
|
语音留言
|
||||||
|
3. 结果页卡片体系还没完全接完
|
||||||
|
结果页最小闭环做过,但还没真正进入:
|
||||||
|
|
||||||
|
原生模板化
|
||||||
|
H5 增强版
|
||||||
|
数据模型与展示模板分层
|
||||||
|
4. 原生有限 DSL 还没开始正式做
|
||||||
|
我们已经把方向定了:
|
||||||
|
|
||||||
|
原生模板
|
||||||
|
原生有限 DSL
|
||||||
|
H5 扩展
|
||||||
|
但目前真正落代码的主要还是原生模板层,DSL 还没开始。
|
||||||
|
|
||||||
|
所以当前阶段判断
|
||||||
|
如果按“卡片架构”来讲,我会定义成:
|
||||||
|
|
||||||
|
已经完成了第一阶段 60%~70%:
|
||||||
|
|
||||||
|
主链和边界已经对了
|
||||||
|
配置和模板能力已经起了
|
||||||
|
还差把它做成真正可长期扩展的“卡片系统”
|
||||||
|
这周最该做什么
|
||||||
|
我建议这周把这 3 件事做完,卡片架构就算基本立住了:
|
||||||
|
|
||||||
|
1. 把原生内容卡正式收成组件化结构
|
||||||
|
目标:
|
||||||
|
|
||||||
|
ContentCardModel
|
||||||
|
模板映射
|
||||||
|
渲染入口统一
|
||||||
|
2. 把 CTA 体系做出来
|
||||||
|
目标:
|
||||||
|
|
||||||
|
CTA 类型
|
||||||
|
CTA 文案
|
||||||
|
CTA 动作模型
|
||||||
|
原生 CTA -> H5 详情/任务
|
||||||
|
3. 把结果页也纳入同一套卡片/模板思路
|
||||||
|
目标:
|
||||||
|
|
||||||
|
原生最小结果页模板化
|
||||||
|
和内容卡共用部分卡片能力
|
||||||
|
为 H5 结果页增强留接口
|
||||||
|
一句话结论
|
||||||
|
现在卡片架构已经从“零散弹层”进化到“有配置、有模板、有 H5 分工的内容系统雏形”了。
|
||||||
|
这周如果继续往下做,最值得的是把:
|
||||||
|
|
||||||
|
原生内容卡组件化
|
||||||
|
CTA 动作体系
|
||||||
|
结果页模板化
|
||||||
|
这三件事补齐。
|
||||||
|
|
||||||
|
如果你愿意,我下一步可以直接给你列一版:
|
||||||
|
本周卡片架构实施清单,按文件和优先级拆开。
|
||||||
|
|
||||||
|
|
||||||
|
好的,列个清单看看
|
||||||
|
|
||||||
|
|
||||||
|
可以,按这周目标,我建议清单拆成 3 层。
|
||||||
|
|
||||||
|
P0 本周必须完成
|
||||||
|
|
||||||
|
原生内容卡模型收口
|
||||||
|
目标:
|
||||||
|
把当前散在地图页状态里的内容卡字段收成统一 ContentCardModel
|
||||||
|
明确字段:
|
||||||
|
template
|
||||||
|
title
|
||||||
|
body
|
||||||
|
motionClass
|
||||||
|
priority
|
||||||
|
once
|
||||||
|
cta
|
||||||
|
建议落点:
|
||||||
|
|
||||||
|
D:\dev\cmr-mini\miniprogram\game\experience
|
||||||
|
D:\dev\cmr-mini\miniprogram\engine\map\mapEngine.ts
|
||||||
|
CTA 动作体系
|
||||||
|
目标:
|
||||||
|
不再只有一个硬编码 查看详情
|
||||||
|
统一成动作模型,例如:
|
||||||
|
open-detail
|
||||||
|
open-task
|
||||||
|
close
|
||||||
|
CTA 支持:
|
||||||
|
label
|
||||||
|
type
|
||||||
|
target
|
||||||
|
建议先只做一主一辅两个按钮位。
|
||||||
|
|
||||||
|
原生内容卡模板注册表
|
||||||
|
目标:
|
||||||
|
把 minimal / story / focus 从“字符串分支”变成正式模板映射
|
||||||
|
先明确每个模板:
|
||||||
|
标题样式
|
||||||
|
正文字数策略
|
||||||
|
CTA 区布局
|
||||||
|
动画等级差异
|
||||||
|
结果页最小模板化
|
||||||
|
目标:
|
||||||
|
把当前结果页按内容卡思路收成一个稳定模板
|
||||||
|
至少区分:
|
||||||
|
result-minimal
|
||||||
|
result-rich 预留
|
||||||
|
P1 本周最好完成
|
||||||
|
|
||||||
|
配置字典补齐卡片字段
|
||||||
|
目标:
|
||||||
|
文档里明确:
|
||||||
|
template
|
||||||
|
contentExperience
|
||||||
|
clickExperience
|
||||||
|
cta
|
||||||
|
默认逻辑
|
||||||
|
样例配置同步更新
|
||||||
|
文件:
|
||||||
|
|
||||||
|
D:\dev\cmr-mini\doc\config-option-dictionary.md
|
||||||
|
D:\dev\cmr-mini\event\classic-sequential.json
|
||||||
|
D:\dev\cmr-mini\event\score-o.json
|
||||||
|
内容卡与结果页的 ViewModel 分层
|
||||||
|
目标:
|
||||||
|
内容卡和结果页都先走 ViewModel
|
||||||
|
页面不直接拼业务字段
|
||||||
|
建议模型:
|
||||||
|
|
||||||
|
ContentCardModel
|
||||||
|
ResultCardModel
|
||||||
|
动画字典补卡片节点
|
||||||
|
目标:
|
||||||
|
把以下节点补进动画字典:
|
||||||
|
内容卡进入
|
||||||
|
内容卡关闭
|
||||||
|
CTA 点击
|
||||||
|
结果页进入
|
||||||
|
结果主数字进入
|
||||||
|
P2 可以放下周
|
||||||
|
|
||||||
|
原生有限 DSL 第一版
|
||||||
|
目标:
|
||||||
|
不直接做万能布局
|
||||||
|
先支持有限区块组合
|
||||||
|
H5 详情/任务页 CTA 扩展
|
||||||
|
目标:
|
||||||
|
不只 查看详情
|
||||||
|
后面支持:
|
||||||
|
拍照打卡
|
||||||
|
语音留言
|
||||||
|
开始互动
|
||||||
|
结果页 H5 增强版入口
|
||||||
|
目标:
|
||||||
|
原生结果页保底
|
||||||
|
H5 作为增强页入口
|
||||||
|
推荐执行顺序
|
||||||
|
|
||||||
|
ContentCardModel
|
||||||
|
CTA 动作体系
|
||||||
|
模板注册表
|
||||||
|
结果页模板化
|
||||||
|
文档和样例同步
|
||||||
|
动画字典补节点
|
||||||
|
一句话
|
||||||
|
这周先把“内容卡是一个系统”这件事做实,再把结果页纳入同一思路。
|
||||||
|
如果你要,我下一步可以直接按这个清单开始做 P0-1:先收 ContentCardModel + CTA 动作体系。
|
||||||
|
|
||||||
|
|
||||||
|
CTA是啥来
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CTA 就是:
|
||||||
|
|
||||||
|
Call To Action
|
||||||
|
中文可以理解成:
|
||||||
|
|
||||||
|
行动按钮
|
||||||
|
引导操作
|
||||||
|
下一步动作
|
||||||
|
在你这个项目里,最直观的例子就是内容卡上的:
|
||||||
|
|
||||||
|
查看详情
|
||||||
|
关闭
|
||||||
|
以后还可以是:
|
||||||
|
|
||||||
|
开始互动
|
||||||
|
拍照打卡
|
||||||
|
语音留言
|
||||||
|
查看结果
|
||||||
|
继续前进
|
||||||
|
也就是说,CTA 不是内容本身,而是用户看到卡片后,可以立刻执行的动作入口。
|
||||||
|
|
||||||
|
在你现在这个项目里
|
||||||
|
例如这张卡:
|
||||||
|
|
||||||
|
标题:比赛开始
|
||||||
|
正文:从这里出发,先熟悉地图方向...
|
||||||
|
CTA:
|
||||||
|
查看详情
|
||||||
|
关闭
|
||||||
|
所以“CTA 动作体系”说白了就是:
|
||||||
|
|
||||||
|
把这些按钮从临时写死,收成统一的动作模型。
|
||||||
|
|
||||||
|
比如以后配置里可以变成:
|
||||||
|
|
||||||
|
{
|
||||||
|
"cta": {
|
||||||
|
"type": "open-detail",
|
||||||
|
"label": "查看详情"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
一句话:
|
||||||
|
CTA 就是卡片上引导用户下一步操作的按钮。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1. 先帮我指定一份游戏的最小配置模板,也就是说,去掉所有选配项,最小的游戏可跑配置,每个选项要带说明
|
||||||
|
2. 再给我一份当前开发状态下最全的配置模板,每个选项,参数要带说明
|
||||||
|
3. 将这2个文档落成文件
|
||||||
|
|
||||||
|
|
||||||
|
先把原生内容卡收口,这是系统默认配置,所有配置不起作用得时候,他们兜底,
|
||||||
|
在原生卡片上,CTA可以有拍照,语音,开启H5深度内容等能力。
|
||||||
|
其中有一个自动出题目的能力,就是2-3位数得加减运算,并提供3个备选项让客户选择,同时有个倒计时,倒计时结束或选择答案后弹出对错提示。答对题目有积分,答错或没答没有积分,正确打点后也收割改点积分,顺序赛默认是1积分,积分赛根据实际点位积分来。
|
||||||
|
先实现以上功能
|
||||||
|
|
||||||
|
|
||||||
|
接着实现几个功能,细节的问题稍后说
|
||||||
|
|
||||||
|
打卡点的样式我需要几套样式,现在是单一标准空心圆圈,太枯燥。
|
||||||
|
我有几个想法:
|
||||||
|
1. 顺序赛,可以定制打卡点样式,可以定制路线腿样式
|
||||||
|
2. 积分赛,可以定制打卡点样式,不同积分可以不同颜色。
|
||||||
|
|
||||||
|
基于上面的想法,你有好的实施方案吗?先讨论
|
||||||
|
|
||||||
|
|
||||||
|
好的,测试可以,接着讨论下一个问题
|
||||||
|
轨迹,我的想法是,用户轨迹有三种形式,无轨迹,全轨迹,拖尾轨迹,如果不走,轨迹最终消失,就是轨迹指着GPS点跑,你有什么方案?先讨论
|
||||||
|
|
||||||
|
轨迹选项:无,彗尾,全轨迹
|
||||||
|
轨迹样式:
|
||||||
|
尾巴:短,中,长
|
||||||
|
颜色:可以放8-16种基本色,亮色
|
||||||
|
|
||||||
|
再说GPS点,用户位置的GPS点样式也是定制的,先说默认样式,可以定制显示与不显示。现在的样式有点呆和粗糙,我想给GPS点上加一个方向指示的小三角,跟着朝向转,你能理解吗?另外GPS点也有3种大小,用户自己可设置,默认中等大小即可,颜色也可设置。最重要的是,根据我们的经验,很多客户希望可以定制这个定位点,具有商业属性,例如换成商家的LOGO,这个有方案吗?先讨论。
|
||||||
|
|
||||||
|
再深一点,自定GPS点能不能做成动画的,停止一个动画,跑起来又是一个动画,甚至可以做些额外的动作。
|
||||||
|
|
||||||
|
开个小差,我想临时加个功能,在咱的GPS模拟器加个日志输出功能,把调试期间不方便打在调试面板里的信息输出到模拟器上,你觉得如何?这样更方便后期调试?如果可以先给个方案
|
||||||
338
doc/config/线上业务接入边界方案.md
Normal file
338
doc/config/线上业务接入边界方案.md
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
# 线上业务接入边界方案
|
||||||
|
|
||||||
|
## 1. 目的
|
||||||
|
|
||||||
|
本文档定义小程序接入线上业务 API 时的架构边界,确保以下原则始终成立:
|
||||||
|
|
||||||
|
- 游戏玩法仍然完全由配置驱动
|
||||||
|
- 线上 API 只负责业务编排,不负责定义或污染玩法
|
||||||
|
- 地图引擎和规则运行时可以继续独立于业务系统运行
|
||||||
|
- 本地 demo、离线配置、线上赛事三种入口可以共存
|
||||||
|
|
||||||
|
本文档适用于以下接入范围:
|
||||||
|
|
||||||
|
- 用户管理
|
||||||
|
- 登录与鉴权
|
||||||
|
- 首页卡片
|
||||||
|
- 赛事详情
|
||||||
|
- 地图详情
|
||||||
|
- Event 详情
|
||||||
|
- 报名
|
||||||
|
- launch 启动
|
||||||
|
- 后续 session 上报与查询
|
||||||
|
|
||||||
|
## 2. 核心结论
|
||||||
|
|
||||||
|
线上接入后,系统仍保持两层结构:
|
||||||
|
|
||||||
|
- 业务层:决定“用户是谁、能进什么、当前启动什么”
|
||||||
|
- 游戏层:决定“地图怎么画、规则怎么跑、控制点怎么判定、体验怎么表现”
|
||||||
|
|
||||||
|
两层之间只允许通过一个明确的启动模型通信,不允许业务 API 对游戏规则对象做直接写入。
|
||||||
|
|
||||||
|
一句话定义:
|
||||||
|
|
||||||
|
> API 负责发放启动上下文,配置负责定义游戏本身。
|
||||||
|
|
||||||
|
## 3. 分层原则
|
||||||
|
|
||||||
|
### 3.1 业务层职责
|
||||||
|
|
||||||
|
业务层负责:
|
||||||
|
|
||||||
|
- 登录与 token 管理
|
||||||
|
- 用户资料与身体数据
|
||||||
|
- 卡片、赛事、地图、Event 列表与详情
|
||||||
|
- 报名资格校验
|
||||||
|
- launch 启动资格与 session 凭证发放
|
||||||
|
- 后续成绩、轨迹、历史记录上传与查询
|
||||||
|
|
||||||
|
业务层可以决定:
|
||||||
|
|
||||||
|
- 是否允许用户启动
|
||||||
|
- 当前应启动哪个 Event
|
||||||
|
- 当前应加载哪份配置或 manifest
|
||||||
|
- 当前启动绑定的 `session_id`、`session_token`
|
||||||
|
|
||||||
|
业务层不可以决定:
|
||||||
|
|
||||||
|
- 控制点布局
|
||||||
|
- 游戏规则
|
||||||
|
- 打卡判定
|
||||||
|
- 跳点规则
|
||||||
|
- 引导、音效、表现策略
|
||||||
|
- 游戏内内容卡的结构和行为定义
|
||||||
|
|
||||||
|
### 3.2 游戏层职责
|
||||||
|
|
||||||
|
游戏层负责:
|
||||||
|
|
||||||
|
- 地图资源加载
|
||||||
|
- KML / course / 配置解析
|
||||||
|
- `GameDefinition` 构建
|
||||||
|
- 规则插件运行
|
||||||
|
- 传感器接入
|
||||||
|
- HUD、反馈、结果页、本地统计
|
||||||
|
- 游戏内 session 生命周期
|
||||||
|
|
||||||
|
游戏层只认配置和本地运行态,不认业务 API 对象。
|
||||||
|
|
||||||
|
游戏层不应该直接出现以下业务字段:
|
||||||
|
|
||||||
|
- `competition_id`
|
||||||
|
- `registration_status`
|
||||||
|
- `access_token`
|
||||||
|
- `refresh_token`
|
||||||
|
- `Authorization`
|
||||||
|
- `user_id`
|
||||||
|
|
||||||
|
业务字段可以存在于页面壳层或业务服务层,但不进入规则层。
|
||||||
|
|
||||||
|
## 4. 唯一允许的层间模型
|
||||||
|
|
||||||
|
业务层和游戏层之间,统一通过 `GameLaunchEnvelope` 通信。
|
||||||
|
|
||||||
|
建议结构如下:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface GameLaunchEnvelope {
|
||||||
|
config: {
|
||||||
|
configUrl: string
|
||||||
|
configLabel: string
|
||||||
|
configChecksumSha256?: string | null
|
||||||
|
releaseId?: string | null
|
||||||
|
routeCode?: string | null
|
||||||
|
}
|
||||||
|
business: {
|
||||||
|
source: 'demo' | 'competition' | 'direct-event' | 'custom'
|
||||||
|
competitionId?: string | null
|
||||||
|
eventId?: string | null
|
||||||
|
launchRequestId?: string | null
|
||||||
|
participantId?: string | null
|
||||||
|
sessionId?: string | null
|
||||||
|
sessionToken?: string | null
|
||||||
|
sessionTokenExpiresAt?: string | null
|
||||||
|
realtimeEndpoint?: string | null
|
||||||
|
realtimeToken?: string | null
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
解释:
|
||||||
|
|
||||||
|
- `config` 是游戏层真正消费的输入
|
||||||
|
- `business` 是业务壳保留的上下文
|
||||||
|
- 地图页可以同时拿到两者,但地图引擎只读取 `config`
|
||||||
|
|
||||||
|
## 5. 推荐启动链路
|
||||||
|
|
||||||
|
### 5.1 Demo 启动
|
||||||
|
|
||||||
|
适用于本地调试、离线测试、玩法验证。
|
||||||
|
|
||||||
|
流程:
|
||||||
|
|
||||||
|
1. 页面构建 demo `GameLaunchEnvelope`
|
||||||
|
2. `config.configUrl` 指向 demo 配置
|
||||||
|
3. `business.source = 'demo'`
|
||||||
|
4. 跳转地图页
|
||||||
|
5. 地图页加载配置并启动引擎
|
||||||
|
|
||||||
|
特点:
|
||||||
|
|
||||||
|
- 不依赖业务 API
|
||||||
|
- 不依赖登录
|
||||||
|
- 不依赖 session
|
||||||
|
|
||||||
|
### 5.2 线上赛事启动
|
||||||
|
|
||||||
|
适用于正式业务入口。
|
||||||
|
|
||||||
|
流程:
|
||||||
|
|
||||||
|
1. 业务页请求赛事 / Event 详情
|
||||||
|
2. 用户在业务页完成登录、资格校验、报名确认
|
||||||
|
3. 用户点击开始,业务页调用 `launch`
|
||||||
|
4. 后端返回 `session_id`、`session_token`、`release_id`、`manifest_url`、`route_code`
|
||||||
|
5. 业务层把上述信息转换为 `GameLaunchEnvelope`
|
||||||
|
6. 地图页只根据 `config` 载入配置
|
||||||
|
7. 业务壳层保存 `business` 上下文供后续上报使用
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- `launch` 是业务启动,不等于规则层 `startSession`
|
||||||
|
- 规则层本地开始游戏,仍由引擎按配置驱动
|
||||||
|
|
||||||
|
### 5.3 线上直入地图启动
|
||||||
|
|
||||||
|
适用于地图详情或 Event 直入。
|
||||||
|
|
||||||
|
流程与赛事入口基本一致,区别仅在于:
|
||||||
|
|
||||||
|
- 入口页不同
|
||||||
|
- 资格校验更轻
|
||||||
|
- `business.source = 'direct-event'`
|
||||||
|
|
||||||
|
## 6. manifest 的角色
|
||||||
|
|
||||||
|
后端提供的 `manifest_url` 不应直接变成规则层对象。
|
||||||
|
|
||||||
|
推荐做法:
|
||||||
|
|
||||||
|
- 业务层或适配层下载 manifest
|
||||||
|
- 将 manifest 解析并映射到当前配置体系
|
||||||
|
- 输出为当前引擎已支持的配置入口
|
||||||
|
|
||||||
|
manifest 是“线上发布描述”,不是“规则运行对象”。
|
||||||
|
|
||||||
|
建议把 manifest 适配理解为一个编译过程:
|
||||||
|
|
||||||
|
- 输入:后端发布描述
|
||||||
|
- 输出:当前配置驱动引擎可识别的配置资源
|
||||||
|
|
||||||
|
## 7. 目录建议
|
||||||
|
|
||||||
|
建议按三层组织代码:
|
||||||
|
|
||||||
|
```text
|
||||||
|
miniprogram/
|
||||||
|
services/
|
||||||
|
http.ts
|
||||||
|
client-api.ts
|
||||||
|
auth.ts
|
||||||
|
business/
|
||||||
|
launch/
|
||||||
|
launchBuilder.ts
|
||||||
|
launchStore.ts
|
||||||
|
manifestAdapter.ts
|
||||||
|
utils/
|
||||||
|
gameLaunch.ts
|
||||||
|
pages/
|
||||||
|
login/
|
||||||
|
home/
|
||||||
|
competition-detail/
|
||||||
|
event-detail/
|
||||||
|
map/
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `services` 只处理 API 通信
|
||||||
|
- `business/launch` 只做业务到配置的适配
|
||||||
|
- `utils/gameLaunch.ts` 定义启动模型和页面跳转协议
|
||||||
|
- `pages/map` 只做配置加载和游戏承载
|
||||||
|
|
||||||
|
## 8. 代码边界约束
|
||||||
|
|
||||||
|
### 8.1 允许进入地图页的内容
|
||||||
|
|
||||||
|
允许进入地图页:
|
||||||
|
|
||||||
|
- `GameLaunchEnvelope`
|
||||||
|
- `configUrl`
|
||||||
|
- `configLabel`
|
||||||
|
- `releaseId`
|
||||||
|
- `routeCode`
|
||||||
|
- `sessionToken`
|
||||||
|
|
||||||
|
但地图页内部还要继续区分:
|
||||||
|
|
||||||
|
- 引擎可读:`config`
|
||||||
|
- 业务壳可读:`business`
|
||||||
|
|
||||||
|
### 8.2 不允许进入引擎的内容
|
||||||
|
|
||||||
|
以下内容禁止进入 `MapEngine`、`GameRuntime`、`GameDefinition`:
|
||||||
|
|
||||||
|
- 用户信息
|
||||||
|
- 登录态 token
|
||||||
|
- 报名状态
|
||||||
|
- 业务接口返回原始对象
|
||||||
|
- 赛事详情原始 JSON
|
||||||
|
- Event 详情原始 JSON
|
||||||
|
|
||||||
|
### 8.3 上报也走旁路
|
||||||
|
|
||||||
|
后续若接 `punches`、`finish`、`session-uploads`,建议流程如下:
|
||||||
|
|
||||||
|
1. 游戏层产生本地事件
|
||||||
|
2. 页面壳层或业务 service 订阅这些事件
|
||||||
|
3. 由业务层决定是否调用 API
|
||||||
|
|
||||||
|
不要在规则层里直接 `wx.request`。
|
||||||
|
|
||||||
|
## 9. 当前项目的落地点
|
||||||
|
|
||||||
|
当前项目已具备以下基础:
|
||||||
|
|
||||||
|
- 地图页是配置驱动入口
|
||||||
|
- `remoteMapConfig.ts` 负责远程配置加载
|
||||||
|
- `MapEngine` 负责本地规则与表现运行
|
||||||
|
- 已新增 `utils/gameLaunch.ts` 作为启动边界模型
|
||||||
|
|
||||||
|
当前建议继续保持:
|
||||||
|
|
||||||
|
- `MapEngine` 只接 `RemoteMapConfig`
|
||||||
|
- 地图页只从 `GameLaunchEnvelope.config` 获取配置入口
|
||||||
|
- 业务上下文保留在地图页外层或页面壳层
|
||||||
|
|
||||||
|
## 10. 分阶段落地建议
|
||||||
|
|
||||||
|
### 阶段一:边界固化
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 地图页彻底改为只接 `GameLaunchEnvelope`
|
||||||
|
- demo 启动与线上启动走同一套入口协议
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
|
||||||
|
- 不再依赖页面内硬编码 URL 作为唯一启动方式
|
||||||
|
- 业务字段不进入引擎
|
||||||
|
|
||||||
|
### 阶段二:业务壳接入
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 接入登录、首页卡片、赛事详情、Event 详情、报名、launch
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
|
||||||
|
- 能从业务页成功进入地图页
|
||||||
|
- 地图仍由配置驱动启动
|
||||||
|
|
||||||
|
### 阶段三:manifest 适配
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 将后端 `manifest_url` 适配为当前配置体系可消费的输入
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
|
||||||
|
- 同一个 Event 的线上发布内容可稳定映射为游戏配置入口
|
||||||
|
|
||||||
|
### 阶段四:session 下游联通
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 补充上报、完成、结果、历史查询
|
||||||
|
|
||||||
|
验收标准:
|
||||||
|
|
||||||
|
- 业务链路打通
|
||||||
|
- 规则层仍不直接依赖业务 API
|
||||||
|
|
||||||
|
## 11. 必须长期坚持的规则
|
||||||
|
|
||||||
|
- 业务 API 不定义玩法
|
||||||
|
- 配置文件不承载用户态
|
||||||
|
- 引擎不依赖登录状态
|
||||||
|
- 引擎不依赖报名状态
|
||||||
|
- 业务页不直接修改 `GameDefinition`
|
||||||
|
- 规则层不直接请求业务 API
|
||||||
|
|
||||||
|
如果后续出现需求需要绕过这几条规则,应视为架构变更,不应当作普通功能迭代处理。
|
||||||
|
|
||||||
|
## 12. 一句话总结
|
||||||
|
|
||||||
|
线上系统负责“把用户送进正确的一局游戏”,配置系统负责“定义这局游戏是什么”。
|
||||||
141
doc/debug/模拟器多通道联调最小方案.md
Normal file
141
doc/debug/模拟器多通道联调最小方案.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# 模拟器多通道联调最小方案
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在不引入房间系统、不增加复杂编排的前提下,让同一台本地模拟器服务能够同时承接多路联调数据,并保证不同联调对象之间的数据不串线。
|
||||||
|
|
||||||
|
## 方案
|
||||||
|
|
||||||
|
统一增加一个字段:
|
||||||
|
|
||||||
|
- `channelId`
|
||||||
|
|
||||||
|
三条链都带这个字段:
|
||||||
|
|
||||||
|
- `mock-gps`
|
||||||
|
- `mock-hr`
|
||||||
|
- `debug-log`
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
- 不做 room / participant 管理
|
||||||
|
- 不做多人控制台编排
|
||||||
|
- 只解决“数据隔离”
|
||||||
|
- GPS、心率、日志三条链统一使用同一个模拟通道号
|
||||||
|
|
||||||
|
## 默认值
|
||||||
|
|
||||||
|
默认通道号:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"default"
|
||||||
|
```
|
||||||
|
|
||||||
|
空值、缺失值都归一化成:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"default"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 消息格式
|
||||||
|
|
||||||
|
### GPS
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "mock_gps",
|
||||||
|
"timestamp": 1712345678901,
|
||||||
|
"channelId": "runner-a",
|
||||||
|
"lat": 31.2304,
|
||||||
|
"lon": 121.4737,
|
||||||
|
"accuracyMeters": 6,
|
||||||
|
"speedMps": 2.4,
|
||||||
|
"headingDeg": 92
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 心率
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "mock_heart_rate",
|
||||||
|
"timestamp": 1712345678901,
|
||||||
|
"channelId": "runner-a",
|
||||||
|
"bpm": 148
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试日志
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "debug-log",
|
||||||
|
"timestamp": 1712345678901,
|
||||||
|
"channelId": "runner-a",
|
||||||
|
"scope": "gps-logo",
|
||||||
|
"level": "info",
|
||||||
|
"message": "logo ready",
|
||||||
|
"payload": {
|
||||||
|
"src": "https://example.com/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模拟器侧
|
||||||
|
|
||||||
|
新版工作台提供一个统一输入:
|
||||||
|
|
||||||
|
- `模拟通道号`
|
||||||
|
|
||||||
|
它会同时作用于:
|
||||||
|
|
||||||
|
- GPS 发送
|
||||||
|
- 心率发送
|
||||||
|
- 日志过滤
|
||||||
|
|
||||||
|
也就是说,一个模拟器页面实例默认对应一个通道。
|
||||||
|
|
||||||
|
## 小程序侧
|
||||||
|
|
||||||
|
调试面板提供一个统一输入:
|
||||||
|
|
||||||
|
- `模拟通道号`
|
||||||
|
|
||||||
|
保存后会同步给:
|
||||||
|
|
||||||
|
- 定位模拟接收过滤
|
||||||
|
- 心率模拟接收过滤
|
||||||
|
- logger 发送通道
|
||||||
|
|
||||||
|
“一键连接开发调试源”会带上当前通道号一起生效。
|
||||||
|
|
||||||
|
## 接收规则
|
||||||
|
|
||||||
|
接收端统一按归一化后的 `channelId` 精确匹配:
|
||||||
|
|
||||||
|
- 收到的消息 `channelId` 与当前模拟通道号一致才消费
|
||||||
|
- 不一致直接忽略
|
||||||
|
|
||||||
|
缺失 `channelId` 的旧消息,按 `default` 处理。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
- 两台手机同时接同一台本地模拟器服务
|
||||||
|
- 一个调试人员同时开多台模拟器页面
|
||||||
|
- 同时联调多个儿童设备
|
||||||
|
|
||||||
|
## 当前边界
|
||||||
|
|
||||||
|
这套最小方案只解决:
|
||||||
|
|
||||||
|
- 多路数据隔离
|
||||||
|
|
||||||
|
不解决:
|
||||||
|
|
||||||
|
- 房间管理
|
||||||
|
- 成员列表
|
||||||
|
- 批量启动/停止
|
||||||
|
- 同步起跑
|
||||||
|
- 多控制台协作
|
||||||
|
|
||||||
|
如果后面真的需要这些,再升级到房间模型。
|
||||||
@@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
在不破坏现有老版面板的前提下,新增一套新版控制面板,用于承接更复杂的开发调试工作流。
|
在不增加第二套历史 UI 负担的前提下,整理出一套新版控制面板,用于承接更复杂的开发调试工作流。
|
||||||
|
|
||||||
重构目标:
|
重构目标:
|
||||||
|
|
||||||
- 保留老版入口,确保已有使用习惯不受影响
|
|
||||||
- 新增工作台式面板,提升连接、控制、观察、排障效率
|
- 新增工作台式面板,提升连接、控制、观察、排障效率
|
||||||
- 继续复用现有模拟器脚本和 websocket 协议,避免维护两套逻辑
|
- 继续复用现有模拟器脚本和 websocket 协议,避免维护两套逻辑
|
||||||
|
- 最终只保留新版入口,避免长期双份维护
|
||||||
|
|
||||||
## 设计原则
|
## 设计原则
|
||||||
|
|
||||||
1. 新旧并行
|
1. 单入口维护
|
||||||
- 新版入口使用 `/`
|
- 新版入口使用 `/`
|
||||||
- 旧版入口保留在 `/v1/`
|
- 模拟器只保留一个工作台入口
|
||||||
2. 逻辑复用
|
2. 逻辑复用
|
||||||
- 两个页面共用 `simulator.js`
|
- 继续复用 `simulator.js`
|
||||||
- 只通过不同 HTML 布局和 CSS 风格区分
|
- 只维护一套 HTML 布局和 CSS 风格
|
||||||
3. 面向调试流程
|
3. 面向调试流程
|
||||||
- 连接优先
|
- 连接优先
|
||||||
- 控制第二
|
- 控制第二
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
- 心率模拟连接状态
|
- 心率模拟连接状态
|
||||||
- 调试日志连接状态
|
- 调试日志连接状态
|
||||||
- 一键连接开发调试源
|
- 一键连接开发调试源
|
||||||
- 新旧面板切换入口
|
|
||||||
|
|
||||||
### 2. 左侧控制区
|
### 2. 左侧控制区
|
||||||
|
|
||||||
@@ -83,26 +82,16 @@
|
|||||||
- 面积更大
|
- 面积更大
|
||||||
- 便于边看地图边看日志
|
- 便于边看地图边看日志
|
||||||
|
|
||||||
## 与旧版的关系
|
|
||||||
|
|
||||||
旧版和新版应同时可用:
|
|
||||||
|
|
||||||
- 新版作为默认工作台
|
|
||||||
- 旧版继续作为稳定基线
|
|
||||||
- 问题排查时可快速回退旧版
|
|
||||||
|
|
||||||
## 实施顺序
|
## 实施顺序
|
||||||
|
|
||||||
1. 根路径切换到新版工作台
|
1. 根路径切换到新版工作台
|
||||||
2. 新增新版样式 `workbench.css`
|
2. 新增新版样式 `workbench.css`
|
||||||
3. 复用现有 `simulator.js`
|
3. 复用现有 `simulator.js`
|
||||||
4. 旧版页面迁移到 `/v1/`
|
4. 清理历史入口和旧文案
|
||||||
5. 在旧版和新版之间互相添加跳转入口
|
5. 更新 README 和调试文档索引
|
||||||
6. 更新 README 和调试文档索引
|
|
||||||
|
|
||||||
## 验收标准
|
## 验收标准
|
||||||
|
|
||||||
- 老版页面继续正常工作
|
|
||||||
- 新版页面可完整使用现有 GPS、心率、日志、路径、网关能力
|
- 新版页面可完整使用现有 GPS、心率、日志、路径、网关能力
|
||||||
- 两个页面共用同一套 websocket 协议和数据逻辑
|
- 模拟器只保留一个工作台入口
|
||||||
- 用户可以在两个版本之间切换
|
- websocket 协议和调试逻辑继续复用
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
复用现有 GPS 模拟器 websocket,在不污染地图调试面板的前提下,把高频、临时、开发期日志输出到外部模拟器。
|
复用现有模拟器服务,在不污染地图调试面板的前提下,把高频、临时、开发期日志输出到外部模拟器。
|
||||||
|
|
||||||
第一阶段只做最小闭环:
|
第一阶段只做最小闭环:
|
||||||
|
|
||||||
- 复用 `tools/mock-gps-sim` 现有 websocket
|
- 复用 `tools/mock-gps-sim` 现有服务
|
||||||
- 增加 `debug-log` 消息类型
|
- 增加 `debug-log` 消息类型
|
||||||
- 小程序侧增加最小 logger
|
- 小程序侧增加最小 logger
|
||||||
- 第一批只发送 `gps-logo` 范围日志
|
- 第一批只发送 `gps-logo` 范围日志
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
{
|
{
|
||||||
"type": "debug-log",
|
"type": "debug-log",
|
||||||
"timestamp": 1712345678901,
|
"timestamp": 1712345678901,
|
||||||
|
"channelId": "runner-a",
|
||||||
"scope": "gps-logo",
|
"scope": "gps-logo",
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"message": "wx.getImageInfo success",
|
"message": "wx.getImageInfo success",
|
||||||
@@ -45,6 +46,8 @@
|
|||||||
毫秒时间戳
|
毫秒时间戳
|
||||||
- `scope`
|
- `scope`
|
||||||
日志分类,例如 `gps-logo`、`h5`、`compass`
|
日志分类,例如 `gps-logo`、`h5`、`compass`
|
||||||
|
- `channelId`
|
||||||
|
日志所属模拟通道,用于多人联调时隔离不同设备的过程日志
|
||||||
- `level`
|
- `level`
|
||||||
`info / warn / error`
|
`info / warn / error`
|
||||||
- `message`
|
- `message`
|
||||||
@@ -123,4 +126,3 @@
|
|||||||
## 当前结论
|
## 当前结论
|
||||||
|
|
||||||
先把 `gps-logo` 调试链打通,再回头用模拟器日志查 logo 为什么不显示,比继续把临时字段堆在调试面板里更稳。
|
先把 `gps-logo` 调试链打通,再回头用模拟器日志查 logo 为什么不显示,比继续把临时字段堆在调试面板里更稳。
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
## 当前主文档
|
## 当前主文档
|
||||||
|
|
||||||
- [模拟器控制面板重构方案](/D:/dev/cmr-mini/doc/debug/模拟器控制面板重构方案.md)
|
- [模拟器控制面板重构方案](/D:/dev/cmr-mini/doc/debug/模拟器控制面板重构方案.md)
|
||||||
用于说明新版模拟器工作台布局、新旧并行策略和重构目标。
|
用于说明新版模拟器工作台布局和重构目标。
|
||||||
- [平台能力说明](/D:/dev/cmr-mini/doc/debug/平台能力说明.md)
|
- [平台能力说明](/D:/dev/cmr-mini/doc/debug/平台能力说明.md)
|
||||||
用于记录主体能力、`web-view`、传感器等平台边界。
|
用于记录主体能力、`web-view`、传感器等平台边界。
|
||||||
- [模拟器调试日志方案](/D:/dev/cmr-mini/doc/debug/模拟器调试日志方案.md)
|
- [模拟器调试日志方案](/D:/dev/cmr-mini/doc/debug/模拟器调试日志方案.md)
|
||||||
用于说明 mock simulator 的日志旁路与 `debug-log` 协议。
|
用于说明 mock simulator 的日志旁路与 `debug-log` 协议。
|
||||||
|
- [模拟器多通道联调最小方案](/D:/dev/cmr-mini/doc/debug/模拟器多通道联调最小方案.md)
|
||||||
|
用于说明 GPS / 心率 / 日志三条链统一按 `channelId` 隔离的最小实现。
|
||||||
- [传感器当前状态总结](/D:/dev/cmr-mini/doc/debug/传感器现状总结.md)
|
- [传感器当前状态总结](/D:/dev/cmr-mini/doc/debug/传感器现状总结.md)
|
||||||
用于看当前已确认的传感器状态与调试结论。
|
用于看当前已确认的传感器状态与调试结论。
|
||||||
- [罗盘问题记录](/D:/dev/cmr-mini/doc/debug/罗盘排障记录.md)
|
- [罗盘问题记录](/D:/dev/cmr-mini/doc/debug/罗盘排障记录.md)
|
||||||
@@ -26,11 +28,13 @@
|
|||||||
2. [mock-simulator-control-panel-proposal.md](/D:/dev/cmr-mini/doc/debug/模拟器控制面板重构方案.md)
|
2. [mock-simulator-control-panel-proposal.md](/D:/dev/cmr-mini/doc/debug/模拟器控制面板重构方案.md)
|
||||||
3. [sensor-current-summary.md](/D:/dev/cmr-mini/doc/debug/传感器现状总结.md)
|
3. [sensor-current-summary.md](/D:/dev/cmr-mini/doc/debug/传感器现状总结.md)
|
||||||
4. [mock-simulator-debug-log-proposal.md](/D:/dev/cmr-mini/doc/debug/模拟器调试日志方案.md)
|
4. [mock-simulator-debug-log-proposal.md](/D:/dev/cmr-mini/doc/debug/模拟器调试日志方案.md)
|
||||||
5. [compass-debugging-notes.md](/D:/dev/cmr-mini/doc/debug/罗盘排障记录.md)
|
5. [multi-channel-simulator-minimal-plan.md](/D:/dev/cmr-mini/doc/debug/模拟器多通道联调最小方案.md)
|
||||||
|
6. [compass-debugging-notes.md](/D:/dev/cmr-mini/doc/debug/罗盘排障记录.md)
|
||||||
|
|
||||||
## 使用建议
|
## 使用建议
|
||||||
|
|
||||||
- 看“当前限制”和“为什么会这样”,优先看平台能力说明。
|
- 看“当前限制”和“为什么会这样”,优先看平台能力说明。
|
||||||
- 看“现在系统是什么状态”,优先看传感器现状总结。
|
- 看“现在系统是什么状态”,优先看传感器现状总结。
|
||||||
- 看“以后日志怎么打”,优先看模拟器日志方案。
|
- 看“以后日志怎么打”,优先看模拟器日志方案。
|
||||||
|
- 看“多人联调怎么隔离”,优先看模拟器多通道联调最小方案。
|
||||||
- 看“为什么罗盘以前坏过”,再去看罗盘问题记录。
|
- 看“为什么罗盘以前坏过”,再去看罗盘问题记录。
|
||||||
|
|||||||
@@ -14,12 +14,18 @@ export interface MockSimulatorDebugLoggerState {
|
|||||||
export interface MockSimulatorDebugLogEntry {
|
export interface MockSimulatorDebugLogEntry {
|
||||||
type: 'debug-log'
|
type: 'debug-log'
|
||||||
timestamp: number
|
timestamp: number
|
||||||
|
channelId?: string
|
||||||
scope: string
|
scope: string
|
||||||
level: MockSimulatorDebugLogLevel
|
level: MockSimulatorDebugLogLevel
|
||||||
message: string
|
message: string
|
||||||
payload?: Record<string, unknown>
|
payload?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMockSimulatorChannelId(rawChannelId: string | null | undefined): string {
|
||||||
|
const trimmed = String(rawChannelId || '').trim()
|
||||||
|
return trimmed || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMockSimulatorLogUrl(rawUrl: string): string {
|
function normalizeMockSimulatorLogUrl(rawUrl: string): string {
|
||||||
const trimmed = String(rawUrl || '').trim()
|
const trimmed = String(rawUrl || '').trim()
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -45,6 +51,7 @@ export class MockSimulatorDebugLogger {
|
|||||||
connected: boolean
|
connected: boolean
|
||||||
connecting: boolean
|
connecting: boolean
|
||||||
url: string
|
url: string
|
||||||
|
channelId: string
|
||||||
queue: MockSimulatorDebugLogEntry[]
|
queue: MockSimulatorDebugLogEntry[]
|
||||||
onStateChange?: (state: MockSimulatorDebugLoggerState) => void
|
onStateChange?: (state: MockSimulatorDebugLoggerState) => void
|
||||||
|
|
||||||
@@ -54,6 +61,7 @@ export class MockSimulatorDebugLogger {
|
|||||||
this.connected = false
|
this.connected = false
|
||||||
this.connecting = false
|
this.connecting = false
|
||||||
this.url = DEFAULT_DEBUG_LOG_URL
|
this.url = DEFAULT_DEBUG_LOG_URL
|
||||||
|
this.channelId = 'default'
|
||||||
this.queue = []
|
this.queue = []
|
||||||
this.onStateChange = onStateChange
|
this.onStateChange = onStateChange
|
||||||
}
|
}
|
||||||
@@ -114,6 +122,10 @@ export class MockSimulatorDebugLogger {
|
|||||||
this.connect()
|
this.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setChannelId(channelId: string): void {
|
||||||
|
this.channelId = normalizeMockSimulatorChannelId(channelId)
|
||||||
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
scope: string,
|
scope: string,
|
||||||
level: MockSimulatorDebugLogLevel,
|
level: MockSimulatorDebugLogLevel,
|
||||||
@@ -127,6 +139,7 @@ export class MockSimulatorDebugLogger {
|
|||||||
const entry: MockSimulatorDebugLogEntry = {
|
const entry: MockSimulatorDebugLogEntry = {
|
||||||
type: 'debug-log',
|
type: 'debug-log',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
channelId: this.channelId,
|
||||||
scope,
|
scope,
|
||||||
level,
|
level,
|
||||||
message,
|
message,
|
||||||
@@ -185,11 +198,13 @@ export class MockSimulatorDebugLogger {
|
|||||||
this.send({
|
this.send({
|
||||||
type: 'debug-log',
|
type: 'debug-log',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
channelId: this.channelId,
|
||||||
scope: 'logger',
|
scope: 'logger',
|
||||||
level: 'info',
|
level: 'info',
|
||||||
message: 'logger channel connected',
|
message: 'logger channel connected',
|
||||||
payload: {
|
payload: {
|
||||||
url: this.url,
|
url: this.url,
|
||||||
|
channelId: this.channelId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.flush()
|
this.flush()
|
||||||
|
|||||||
@@ -46,5 +46,5 @@
|
|||||||
"ignore": [],
|
"ignore": [],
|
||||||
"include": []
|
"include": []
|
||||||
},
|
},
|
||||||
"appid": "wx9cca5c5a219a4f9c"
|
"appid": "wx0c8b079993bb9d7a"
|
||||||
}
|
}
|
||||||
694
tmp/Client-API.md
Normal file
694
tmp/Client-API.md
Normal file
@@ -0,0 +1,694 @@
|
|||||||
|
# Client API 前端联调文档
|
||||||
|
|
||||||
|
文档版本:v1.0.0
|
||||||
|
最后更新:2026-03-31
|
||||||
|
状态:联调中
|
||||||
|
|
||||||
|
## 1. 文档说明
|
||||||
|
|
||||||
|
本文档面向前端联调,描述当前 `client-api` 在代码中真实可用的接口。
|
||||||
|
|
||||||
|
约定:
|
||||||
|
|
||||||
|
- 本文档优先级高于产品阶段的总草案;前端联调以本文档为准
|
||||||
|
- 本文档只覆盖 `client-api`
|
||||||
|
- 每个接口会标记当前状态:
|
||||||
|
- `已实现,已联调`
|
||||||
|
- `已实现,未联调`
|
||||||
|
- `预留未就绪`
|
||||||
|
|
||||||
|
## 2. 通用约定
|
||||||
|
|
||||||
|
### 2.1 Base Path
|
||||||
|
|
||||||
|
- `client-api`:`/client/v1`
|
||||||
|
|
||||||
|
### 2.2 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"request_id": "req_xxx",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 失败响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"request_id": "req_xxx",
|
||||||
|
"error": {
|
||||||
|
"code": "invalid_request",
|
||||||
|
"message": "xxx",
|
||||||
|
"details": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 鉴权说明
|
||||||
|
|
||||||
|
- 登录后接口使用:`Authorization: Bearer {access_token}`
|
||||||
|
- `launch` 成功后会返回 `session_token`
|
||||||
|
- 但当前版本下游 `session` 相关接口尚未开放,因此前端暂时只需要保存 `session_token`
|
||||||
|
|
||||||
|
### 2.5 枚举说明
|
||||||
|
|
||||||
|
`client_type`
|
||||||
|
|
||||||
|
- `app`
|
||||||
|
- `wechat`
|
||||||
|
|
||||||
|
`body_profile_status`
|
||||||
|
|
||||||
|
- `pending`
|
||||||
|
- `completed`
|
||||||
|
|
||||||
|
## 3. 已实现接口
|
||||||
|
|
||||||
|
### 3.1 `POST /client/v1/auth/sms/send`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 发送短信验证码
|
||||||
|
- 当前已接阿里云短信
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `scene` | `string` | 是 | 当前已使用 `client_login` |
|
||||||
|
| `country_code` | `string` | 是 | 国家码,如 `86` |
|
||||||
|
| `mobile` | `string` | 是 | 手机号 |
|
||||||
|
| `client_type` | `string` | 是 | `app` / `wechat` |
|
||||||
|
| `device_id` | `string` | 是 | 设备唯一标识 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scene": "client_login",
|
||||||
|
"country_code": "86",
|
||||||
|
"mobile": "15168870729",
|
||||||
|
"client_type": "app",
|
||||||
|
"device_id": "dev_iphone_001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `cooldown_sec` | `int` | 重发冷却时间 |
|
||||||
|
| `expires_in_sec` | `int` | 验证码有效期 |
|
||||||
|
|
||||||
|
### 3.2 `POST /client/v1/auth/login/sms`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 短信登录
|
||||||
|
- 如果手机号首次登录,后端会自动创建:
|
||||||
|
- `users`
|
||||||
|
- `login_identities`
|
||||||
|
- 默认 `user_body_profiles`
|
||||||
|
- 首条 `user_body_profile_versions`
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `login_request_id` | `string` | 是 | 登录幂等号 |
|
||||||
|
| `country_code` | `string` | 是 | 国家码 |
|
||||||
|
| `mobile` | `string` | 是 | 手机号 |
|
||||||
|
| `sms_code` | `string` | 是 | 短信验证码 |
|
||||||
|
| `client_type` | `string` | 是 | `app` / `wechat` |
|
||||||
|
| `device_id` | `string` | 是 | 设备唯一标识 |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `user_id` | `string` | 用户公开 ID |
|
||||||
|
| `access_token` | `string` | 登录态 token |
|
||||||
|
| `refresh_token` | `string` | refresh token |
|
||||||
|
| `login_method` | `string` | 当前为 `sms` |
|
||||||
|
| `body_profile_status` | `string` | `pending` / `completed` |
|
||||||
|
| `expires_in_sec` | `int` | `access_token` 有效期 |
|
||||||
|
|
||||||
|
### 3.3 `POST /client/v1/auth/login/wechat`
|
||||||
|
|
||||||
|
状态:`已实现,未联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 微信登录入口已预留
|
||||||
|
- 当前仓库里 provider 仍以 mock 为主,尚未做真实联调
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `login_request_id` | `string` | 是 | 登录幂等号 |
|
||||||
|
| `wechat_login_code` | `string` | 是 | 微信登录 code |
|
||||||
|
| `client_type` | `string` | 是 | 建议传 `wechat` |
|
||||||
|
| `device_id` | `string` | 是 | 设备唯一标识 |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `user_id` | `string` | 用户公开 ID |
|
||||||
|
| `access_token` | `string` | 登录态 token |
|
||||||
|
| `refresh_token` | `string` | refresh token |
|
||||||
|
| `login_method` | `string` | 当前为 `wechat` |
|
||||||
|
| `body_profile_status` | `string` | `pending` / `completed` |
|
||||||
|
| `expires_in_sec` | `int` | `access_token` 有效期 |
|
||||||
|
|
||||||
|
### 3.4 `POST /client/v1/auth/refresh`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 使用 refresh token 刷新 `access_token`
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `refresh_token` | `string` | 是 | refresh token |
|
||||||
|
| `client_type` | `string` | 是 | `app` / `wechat` |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `access_token` | `string` | 新的登录态 token |
|
||||||
|
| `expires_in_sec` | `int` | 有效期 |
|
||||||
|
|
||||||
|
### 3.5 `PUT /client/v1/me/body-profile`
|
||||||
|
|
||||||
|
状态:`已实现,未联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 更新身体数据
|
||||||
|
- 成功后会更新当前档案,并追加历史版本
|
||||||
|
|
||||||
|
认证:
|
||||||
|
|
||||||
|
- 需要 `access_token`
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `gender` | `string` | 否 | `male` / `female` / `other` / `unknown` |
|
||||||
|
| `birth_date` | `string` | 否 | `YYYY-MM-DD` |
|
||||||
|
| `height_cm` | `number` | 否 | 身高,厘米 |
|
||||||
|
| `weight_kg` | `number` | 否 | 体重,千克 |
|
||||||
|
| `resting_heart_rate_bpm` | `int` | 否 | 静息心率 |
|
||||||
|
| `max_heart_rate_bpm` | `int` | 否 | 最大心率 |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `user_id` | `string` | 用户公开 ID |
|
||||||
|
| `body_profile_status` | `string` | `pending` / `completed` |
|
||||||
|
| `completed_at` | `string` | 首次补全时间,未补全时可能为空 |
|
||||||
|
|
||||||
|
### 3.6 `GET /client/v1/cards`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 首页卡片列表
|
||||||
|
|
||||||
|
查询参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `region_code` | `string` | 否 | 地区编码,当前可空 |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
`data.items[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `card_id` | `string` | 卡片公开 ID |
|
||||||
|
| `card_type` | `string` | 例如 `competition_card` |
|
||||||
|
| `display_name` | `string` | 卡片展示名称 |
|
||||||
|
| `competition_id` | `string` | 关联赛事 ID,非赛事卡可能为空 |
|
||||||
|
| `html_url` | `string` | H5 地址,可空 |
|
||||||
|
| `cover_url` | `string` | 封面地址,可空 |
|
||||||
|
| `display_slot` | `string` | 展示槽位 |
|
||||||
|
| `display_priority` | `int` | 展示优先级 |
|
||||||
|
|
||||||
|
### 3.7 `GET /client/v1/competitions/{competition_id}`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 赛事详情页
|
||||||
|
- 如果带 `access_token`,会返回当前用户的 `registration_status`
|
||||||
|
|
||||||
|
认证:
|
||||||
|
|
||||||
|
- 可匿名访问
|
||||||
|
- 建议前端在登录后带上 `access_token`
|
||||||
|
|
||||||
|
Path 参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `competition_id` | `string` | 赛事公开 ID |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `competition_id` | `string` | 赛事公开 ID |
|
||||||
|
| `display_name` | `string` | 赛事名称 |
|
||||||
|
| `competition_status` | `string` | 当前状态 |
|
||||||
|
| `registration_enabled` | `bool` | 是否允许报名 |
|
||||||
|
| `registration_status` | `string` | 当前用户报名状态,匿名访问时可能为空 |
|
||||||
|
| `competition_start_at` | `string` | 赛事开始时间 |
|
||||||
|
| `competition_end_at` | `string` | 赛事结束时间 |
|
||||||
|
| `leaderboard_enabled` | `bool` | 是否展示排行榜 |
|
||||||
|
| `realtime_board_enabled` | `bool` | 是否启用实时榜 |
|
||||||
|
|
||||||
|
`data.events[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
| `display_name` | `string` | Event 名称 |
|
||||||
|
| `is_default` | `bool` | 是否默认 Event |
|
||||||
|
| `current_release_id` | `string` | 当前发布版 ID |
|
||||||
|
| `manifest_url` | `string` | manifest 下载地址 |
|
||||||
|
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
|
||||||
|
| `relation_status` | `string` | 关联状态 |
|
||||||
|
|
||||||
|
### 3.8 `GET /client/v1/competitions/{competition_id}/events/{event_id}`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 赛事上下文下的 Event 详情
|
||||||
|
- 前端应在这个页面预加载 manifest,并完成路线预览
|
||||||
|
|
||||||
|
认证:
|
||||||
|
|
||||||
|
- 可匿名访问
|
||||||
|
- 建议前端在登录后带上 `access_token`
|
||||||
|
|
||||||
|
Path 参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `competition_id` | `string` | 赛事公开 ID |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `competition_id` | `string` | 赛事公开 ID |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
| `display_name` | `string` | Event 名称 |
|
||||||
|
| `current_release_id` | `string` | 当前发布版 ID |
|
||||||
|
| `manifest_url` | `string` | manifest 下载地址 |
|
||||||
|
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
|
||||||
|
| `direct_entry_enabled` | `bool` | 是否支持地图直入 |
|
||||||
|
| `playfield_version_id` | `string` | 场地版本 ID |
|
||||||
|
| `playfield_kind` | `string` | 如 `course` |
|
||||||
|
|
||||||
|
`data.competition_context`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `competition_id` | `string` | 赛事公开 ID |
|
||||||
|
| `display_name` | `string` | 赛事名称 |
|
||||||
|
| `competition_status` | `string` | 赛事状态 |
|
||||||
|
| `registration_status` | `string` | 当前用户报名状态 |
|
||||||
|
| `leaderboard_enabled` | `bool` | 是否显示排行榜 |
|
||||||
|
| `realtime_board_enabled` | `bool` | 是否启用实时榜 |
|
||||||
|
|
||||||
|
`data.map_summary`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `map_id` | `string` | 地图公开 ID |
|
||||||
|
| `display_name` | `string` | 地图名称 |
|
||||||
|
| `cover_url` | `string` | 封面图,可空 |
|
||||||
|
| `scale_text` | `string` | 比例尺,可空 |
|
||||||
|
|
||||||
|
`data.preview_summary`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `control_count` | `int` | 控制点数量 |
|
||||||
|
| `route_count` | `int` | 路线数量 |
|
||||||
|
| `playfield_kind` | `string` | 场地类型 |
|
||||||
|
|
||||||
|
### 3.9 `POST /client/v1/competitions/{competition_id}/registrations`
|
||||||
|
|
||||||
|
状态:`已实现,未联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 赛事报名
|
||||||
|
|
||||||
|
认证:
|
||||||
|
|
||||||
|
- 需要 `access_token`
|
||||||
|
|
||||||
|
Path 参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `competition_id` | `string` | 赛事公开 ID |
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `group_id` | `string` | 否 | 队伍或分组 ID |
|
||||||
|
| `form_payload` | `object` | 否 | 附加报名表单 |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `registration_id` | `string` | 报名记录 ID |
|
||||||
|
| `status` | `string` | 当前报名状态 |
|
||||||
|
|
||||||
|
### 3.10 `GET /client/v1/maps`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 地图列表页
|
||||||
|
|
||||||
|
查询参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `region_code` | `string` | 否 | 地区编码 |
|
||||||
|
| `page` | `int` | 否 | 默认 `1` |
|
||||||
|
| `page_size` | `int` | 否 | 默认 `20`,最大 `50` |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
`data.items[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `map_id` | `string` | 地图公开 ID |
|
||||||
|
| `display_name` | `string` | 地图名称 |
|
||||||
|
| `cover_url` | `string` | 封面图,可空 |
|
||||||
|
| `scale_text` | `string` | 比例尺,可空 |
|
||||||
|
| `distance_from_user_km` | `number` | 距离,可空 |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 当前响应只返回 `items`,不回显 `page/page_size/total`
|
||||||
|
|
||||||
|
### 3.11 `GET /client/v1/maps/{map_id}`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 地图详情页
|
||||||
|
- 同时返回当前地图下允许直入的 Event 列表
|
||||||
|
|
||||||
|
Path 参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `map_id` | `string` | 地图公开 ID |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `map_id` | `string` | 地图公开 ID |
|
||||||
|
| `display_name` | `string` | 地图名称 |
|
||||||
|
|
||||||
|
`data.map_summary`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `cover_url` | `string` | 封面图,可空 |
|
||||||
|
| `scale_text` | `string` | 比例尺,可空 |
|
||||||
|
| `updated_date` | `string` | 更新时间,可空 |
|
||||||
|
|
||||||
|
`data.events[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
| `display_name` | `string` | Event 名称 |
|
||||||
|
| `preview_image_url` | `string` | 预览图,可空 |
|
||||||
|
| `control_count` | `int` | 控制点数量 |
|
||||||
|
| `route_count` | `int` | 路线数量 |
|
||||||
|
| `direct_entry_enabled` | `bool` | 是否允许地图直入 |
|
||||||
|
| `current_release_id` | `string` | 当前发布版 ID |
|
||||||
|
| `manifest_url` | `string` | manifest 下载地址 |
|
||||||
|
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
|
||||||
|
| `playfield_kind` | `string` | 如 `course` |
|
||||||
|
|
||||||
|
### 3.12 `GET /client/v1/events`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 地图直入链路下的 Event 列表
|
||||||
|
|
||||||
|
查询参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `map_id` | `string` | 否 | 按地图筛选 |
|
||||||
|
| `page` | `int` | 否 | 默认 `1` |
|
||||||
|
| `page_size` | `int` | 否 | 默认 `20`,最大 `50` |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
`data.items[]`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
| `display_name` | `string` | Event 名称 |
|
||||||
|
| `map_id` | `string` | 地图公开 ID |
|
||||||
|
| `map_display_name` | `string` | 地图名称 |
|
||||||
|
| `preview_image_url` | `string` | 预览图,可空 |
|
||||||
|
| `control_count` | `int` | 控制点数量 |
|
||||||
|
| `route_count` | `int` | 路线数量 |
|
||||||
|
| `direct_entry_enabled` | `bool` | 是否允许地图直入 |
|
||||||
|
| `current_release_id` | `string` | 当前发布版 ID |
|
||||||
|
| `manifest_url` | `string` | manifest 下载地址 |
|
||||||
|
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
|
||||||
|
| `playfield_kind` | `string` | 如 `course` |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 当前响应只返回 `items`,不回显 `page/page_size/total`
|
||||||
|
|
||||||
|
### 3.13 `GET /client/v1/events/{event_id}`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 地图直入上下文下的 Event 详情
|
||||||
|
- 与赛事入口页共用同一套 Event 详情视图
|
||||||
|
|
||||||
|
Path 参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
| `display_name` | `string` | Event 名称 |
|
||||||
|
| `direct_entry_enabled` | `bool` | 是否允许地图直入 |
|
||||||
|
| `current_release_id` | `string` | 当前发布版 ID |
|
||||||
|
| `manifest_url` | `string` | manifest 下载地址 |
|
||||||
|
| `manifest_checksum_sha256` | `string` | manifest 校验值 |
|
||||||
|
| `playfield_version_id` | `string` | 场地版本 ID |
|
||||||
|
| `playfield_kind` | `string` | 如 `course` |
|
||||||
|
|
||||||
|
`data.map_summary`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `map_id` | `string` | 地图公开 ID |
|
||||||
|
| `display_name` | `string` | 地图名称 |
|
||||||
|
| `cover_url` | `string` | 封面图,可空 |
|
||||||
|
| `scale_text` | `string` | 比例尺,可空 |
|
||||||
|
|
||||||
|
`data.event_summary`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `control_count` | `int` | 控制点数量 |
|
||||||
|
| `route_count` | `int` | 路线数量 |
|
||||||
|
| `playfield_kind` | `string` | 场地类型 |
|
||||||
|
|
||||||
|
### 3.14 `POST /client/v1/competitions/{competition_id}/events/{event_id}/launch`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 赛事入口 `launch`
|
||||||
|
- 会校验赛事时间窗、报名状态、`release_id`
|
||||||
|
- 成功后创建 session,并返回 `session_token`
|
||||||
|
|
||||||
|
认证:
|
||||||
|
|
||||||
|
- 需要 `access_token`
|
||||||
|
|
||||||
|
Path 参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `competition_id` | `string` | 赛事公开 ID |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `launch_request_id` | `string` | 是 | 启动幂等号 |
|
||||||
|
| `device_id` | `string` | 是 | 设备唯一标识 |
|
||||||
|
| `client_type` | `string` | 是 | 必须与 `access_token` 内声明一致 |
|
||||||
|
| `release_id` | `string` | 是 | 前端当前持有的发布版 ID |
|
||||||
|
| `route_code` | `string` | 否 | 当前选中的路线编码 |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `session_id` | `string` | 会话 ID |
|
||||||
|
| `session_token` | `string` | 会话 token |
|
||||||
|
| `session_token_expires_at` | `string` | 会话 token 过期时间 |
|
||||||
|
| `participant_id` | `string` | 参赛身份 ID |
|
||||||
|
| `competition_id` | `string` | 赛事公开 ID |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
| `event_release_id` | `string` | 实际冻结的发布版 ID |
|
||||||
|
| `playfield_version_id` | `string` | 场地版本 ID |
|
||||||
|
| `route_code` | `string` | 当前冻结的路线编码 |
|
||||||
|
| `realtime_endpoint` | `string` | realtime-center 地址 |
|
||||||
|
| `realtime_token` | `string` | 当前版本通常为空 |
|
||||||
|
|
||||||
|
当前注意:
|
||||||
|
|
||||||
|
- 若前端传入的 `release_id` 已过期,会返回 `EVENT_RELEASE_STALE`
|
||||||
|
- 当前后端会冻结并回传 `route_code`,但还没有对 manifest 内路线做严格校验
|
||||||
|
|
||||||
|
### 3.15 `POST /client/v1/events/{event_id}/launch`
|
||||||
|
|
||||||
|
状态:`已实现,已联调`
|
||||||
|
|
||||||
|
接口介绍:
|
||||||
|
|
||||||
|
- 地图直入 `launch`
|
||||||
|
- 不校验赛事报名资格
|
||||||
|
- 成功后创建 session,并返回 `session_token`
|
||||||
|
|
||||||
|
认证:
|
||||||
|
|
||||||
|
- 需要 `access_token`
|
||||||
|
|
||||||
|
Path 参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `launch_request_id` | `string` | 是 | 启动幂等号 |
|
||||||
|
| `device_id` | `string` | 是 | 设备唯一标识 |
|
||||||
|
| `client_type` | `string` | 是 | 必须与 `access_token` 内声明一致 |
|
||||||
|
| `release_id` | `string` | 是 | 前端当前持有的发布版 ID |
|
||||||
|
| `route_code` | `string` | 否 | 当前选中的路线编码 |
|
||||||
|
|
||||||
|
返回参数:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `session_id` | `string` | 会话 ID |
|
||||||
|
| `session_token` | `string` | 会话 token |
|
||||||
|
| `session_token_expires_at` | `string` | 会话 token 过期时间 |
|
||||||
|
| `participant_id` | `string` | 参赛身份 ID |
|
||||||
|
| `event_id` | `string` | Event 公开 ID |
|
||||||
|
| `event_release_id` | `string` | 实际冻结的发布版 ID |
|
||||||
|
| `playfield_version_id` | `string` | 场地版本 ID |
|
||||||
|
| `route_code` | `string` | 当前冻结的路线编码 |
|
||||||
|
| `realtime_endpoint` | `string` | realtime-center 地址 |
|
||||||
|
| `realtime_token` | `string` | 当前版本通常为空 |
|
||||||
|
|
||||||
|
当前注意:
|
||||||
|
|
||||||
|
- 若前端传入的 `release_id` 已过期,会返回 `EVENT_RELEASE_STALE`
|
||||||
|
- 当前后端会冻结并回传 `route_code`,但还没有对 manifest 内路线做严格校验
|
||||||
|
|
||||||
|
## 4. 预留未就绪接口
|
||||||
|
|
||||||
|
以下接口当前在路由中已占位,但实际会直接返回 `not implemented`:
|
||||||
|
|
||||||
|
| 接口 | 当前错误码 |
|
||||||
|
| --- | --- |
|
||||||
|
| `POST /client/v1/session-uploads` | `session_upload_not_ready` |
|
||||||
|
| `POST /client/v1/punches` | `session_punch_not_ready` |
|
||||||
|
| `POST /client/v1/sessions/{session_id}/finish` | `session_finish_not_ready` |
|
||||||
|
| `GET /client/v1/sessions/{session_id}/result` | `session_result_not_ready` |
|
||||||
|
| `GET /client/v1/sessions/{session_id}/replay-summary` | `session_replay_summary_not_ready` |
|
||||||
|
| `GET /client/v1/sessions/{session_id}/gps-track` | `session_gps_track_not_ready` |
|
||||||
|
| `GET /client/v1/sessions/{session_id}/heart-rate` | `session_heart_rate_not_ready` |
|
||||||
|
| `GET /client/v1/me/sessions` | `session_history_not_ready` |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 所以当前不是“只有 GPS / 心率上报没测”
|
||||||
|
- 而是整条 `session` 下游链路都还没开放
|
||||||
|
- 当前已实测闭环停在 `launch` 成功返回 `session_token`
|
||||||
|
|
||||||
|
## 5. 当前测试数据
|
||||||
|
|
||||||
|
以下数据可直接用于本地联调:
|
||||||
|
|
||||||
|
| 类型 | ID |
|
||||||
|
| --- | --- |
|
||||||
|
| `card_id` | `crd_classic_demo_001` |
|
||||||
|
| `competition_id` | `cmp_classic_demo_001` |
|
||||||
|
| `map_id` | `lxcb-001` |
|
||||||
|
| `event_id` | `sample-classic-001` |
|
||||||
|
| `release_id` | `sample-classic-001-rel-1` |
|
||||||
|
| `route_code` | `classic-001` |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 已存在测试赛事卡片、地图、Event、manifest 绑定关系
|
||||||
|
- 已存在一个已批准报名的测试用户,可直接验证赛事入口 `launch`
|
||||||
@@ -11,12 +11,36 @@ npm run mock-gps-sim
|
|||||||
启动后:
|
启动后:
|
||||||
|
|
||||||
- 新版工作台: `http://127.0.0.1:17865/`
|
- 新版工作台: `http://127.0.0.1:17865/`
|
||||||
- 旧版面板: `http://127.0.0.1:17865/v1/`
|
|
||||||
- 小程序定位模拟地址: `ws://127.0.0.1:17865/mock-gps`
|
- 小程序定位模拟地址: `ws://127.0.0.1:17865/mock-gps`
|
||||||
- 小程序心率模拟地址: `ws://127.0.0.1:17865/mock-hr`
|
- 小程序心率模拟地址: `ws://127.0.0.1:17865/mock-hr`
|
||||||
- 小程序调试日志地址: `ws://127.0.0.1:17865/debug-log`
|
- 小程序调试日志地址: `ws://127.0.0.1:17865/debug-log`
|
||||||
- 资源代理: `http://127.0.0.1:17865/proxy?url=<remote-url>`
|
- 资源代理: `http://127.0.0.1:17865/proxy?url=<remote-url>`
|
||||||
|
|
||||||
|
## 多通道联调
|
||||||
|
|
||||||
|
模拟器现在支持一个最小的多通道隔离方案:
|
||||||
|
|
||||||
|
- GPS 模拟消息带 `channelId`
|
||||||
|
- 心率模拟消息带 `channelId`
|
||||||
|
- 调试日志消息带 `channelId`
|
||||||
|
- 小程序端按同一个模拟通道号过滤三条链
|
||||||
|
|
||||||
|
默认通道号:
|
||||||
|
|
||||||
|
```text
|
||||||
|
default
|
||||||
|
```
|
||||||
|
|
||||||
|
如果需要多人并行联调,可以在模拟器工作台里把“模拟通道号”改成例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runner-a
|
||||||
|
runner-b
|
||||||
|
group-01
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在小程序调试面板里把“模拟通道号”也配成同一个值。
|
||||||
|
|
||||||
## 当前能力
|
## 当前能力
|
||||||
|
|
||||||
- 直接载入 `game.json`
|
- 直接载入 `game.json`
|
||||||
@@ -47,6 +71,7 @@ ws://127.0.0.1:17865/debug-log
|
|||||||
{
|
{
|
||||||
"type": "debug-log",
|
"type": "debug-log",
|
||||||
"timestamp": 1712345678901,
|
"timestamp": 1712345678901,
|
||||||
|
"channelId": "runner-a",
|
||||||
"scope": "gps-logo",
|
"scope": "gps-logo",
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"message": "wx.getImageInfo success",
|
"message": "wx.getImageInfo success",
|
||||||
@@ -89,12 +114,6 @@ ws://127.0.0.1:17865/debug-log
|
|||||||
http://127.0.0.1:17865/
|
http://127.0.0.1:17865/
|
||||||
```
|
```
|
||||||
|
|
||||||
如果需要旧版稳定界面,打开:
|
|
||||||
|
|
||||||
```text
|
|
||||||
http://127.0.0.1:17865/v1/
|
|
||||||
```
|
|
||||||
|
|
||||||
在“新网关桥接”区域可以直接配置:
|
在“新网关桥接”区域可以直接配置:
|
||||||
|
|
||||||
- 是否启用桥接
|
- 是否启用桥接
|
||||||
|
|||||||
@@ -14,10 +14,16 @@
|
|||||||
<div class="wb-topbar__eyebrow">MOCK GPS SIM</div>
|
<div class="wb-topbar__eyebrow">MOCK GPS SIM</div>
|
||||||
<h1>模拟器工作台</h1>
|
<h1>模拟器工作台</h1>
|
||||||
<div class="wb-topbar__links">
|
<div class="wb-topbar__links">
|
||||||
<a href="/v1/">打开旧版面板</a>
|
<div id="socketStatus" class="badge badge--muted">未连接</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wb-topbar__status">
|
<div class="wb-topbar__status">
|
||||||
|
<div class="wb-topbar__global">
|
||||||
|
<label class="field wb-topbar__field">
|
||||||
|
<span>模拟通道号</span>
|
||||||
|
<input id="simChannelIdInput" type="text" placeholder="default / runner-a">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="wb-connection-bar">
|
<div class="wb-connection-bar">
|
||||||
<div class="wb-connection-pill">
|
<div class="wb-connection-pill">
|
||||||
<span class="wb-connection-pill__label">定位模拟</span>
|
<span class="wb-connection-pill__label">定位模拟</span>
|
||||||
@@ -36,29 +42,11 @@
|
|||||||
<strong id="topGatewayStatus" class="wb-connection-pill__value">未启用</strong>
|
<strong id="topGatewayStatus" class="wb-connection-pill__value">未启用</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="socketStatus" class="badge badge--muted">未连接</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="wb-layout">
|
<div class="wb-layout">
|
||||||
<aside class="wb-sidebar">
|
<aside class="wb-sidebar">
|
||||||
<section class="wb-card">
|
|
||||||
<div class="wb-card__title">运行摘要</div>
|
|
||||||
<div class="stat"><span>资源状态</span><strong id="summaryResourceText">未载入</strong></div>
|
|
||||||
<div class="stat"><span>定位发送</span><strong id="summaryGpsSendText">待命</strong></div>
|
|
||||||
<div class="stat"><span>心率发送</span><strong id="summaryHrSendText">待命</strong></div>
|
|
||||||
<div class="stat"><span>路径状态</span><strong id="summaryPathText">待命</strong></div>
|
|
||||||
<div class="stat"><span>网关桥接</span><strong id="summaryGatewayText">未启用</strong></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="wb-card">
|
|
||||||
<div class="wb-card__title">当前位置</div>
|
|
||||||
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
|
|
||||||
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
|
|
||||||
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
|
|
||||||
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<details class="wb-section" open>
|
<details class="wb-section" open>
|
||||||
<summary>资源加载</summary>
|
<summary>资源加载</summary>
|
||||||
<div class="wb-section__body">
|
<div class="wb-section__body">
|
||||||
@@ -274,13 +262,31 @@
|
|||||||
<div id="debugLog" class="log log--debug log--floating"></div>
|
<div id="debugLog" class="log log--debug log--floating"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
|
||||||
<section class="wb-bottom-strip">
|
<aside class="wb-rail">
|
||||||
<section class="wb-card wb-card--bottom">
|
<section class="wb-card">
|
||||||
|
<div class="wb-card__title">运行摘要</div>
|
||||||
|
<div class="stat"><span>资源状态</span><strong id="summaryResourceText">未载入</strong></div>
|
||||||
|
<div class="stat"><span>定位发送</span><strong id="summaryGpsSendText">待命</strong></div>
|
||||||
|
<div class="stat"><span>心率发送</span><strong id="summaryHrSendText">待命</strong></div>
|
||||||
|
<div class="stat"><span>路径状态</span><strong id="summaryPathText">待命</strong></div>
|
||||||
|
<div class="stat"><span>网关桥接</span><strong id="summaryGatewayText">未启用</strong></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="wb-card">
|
||||||
|
<div class="wb-card__title">当前位置</div>
|
||||||
|
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
|
||||||
|
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
|
||||||
|
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
|
||||||
|
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="wb-card">
|
||||||
<div class="wb-card__title">最近事件</div>
|
<div class="wb-card__title">最近事件</div>
|
||||||
<div id="log" class="log"></div>
|
<div id="log" class="log"></div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
|||||||
@@ -13,8 +13,14 @@
|
|||||||
])
|
])
|
||||||
const BRIDGE_CONFIG_STORAGE_KEY = 'mock-gps-sim.bridge-config'
|
const BRIDGE_CONFIG_STORAGE_KEY = 'mock-gps-sim.bridge-config'
|
||||||
const BRIDGE_PRESETS_STORAGE_KEY = 'mock-gps-sim.bridge-presets'
|
const BRIDGE_PRESETS_STORAGE_KEY = 'mock-gps-sim.bridge-presets'
|
||||||
|
const SIM_CHANNEL_STORAGE_KEY = 'mock-gps-sim.channel-id'
|
||||||
const MAX_DEBUG_LOG_LINES = 400
|
const MAX_DEBUG_LOG_LINES = 400
|
||||||
|
|
||||||
|
function normalizeSimChannelId(rawValue) {
|
||||||
|
const trimmed = String(rawValue || '').trim()
|
||||||
|
return trimmed || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
const map = L.map('map').setView(DEFAULT_CENTER, 16)
|
const map = L.map('map').setView(DEFAULT_CENTER, 16)
|
||||||
let tileLayer = createTileLayer(DEFAULT_TILE_URL, {
|
let tileLayer = createTileLayer(DEFAULT_TILE_URL, {
|
||||||
maxZoom: 20,
|
maxZoom: 20,
|
||||||
@@ -60,6 +66,7 @@
|
|||||||
lastHeartRateSentText: '--',
|
lastHeartRateSentText: '--',
|
||||||
lastResourceDetailText: '尚未载入资源',
|
lastResourceDetailText: '尚未载入资源',
|
||||||
lastTrackSourceText: '路径待命',
|
lastTrackSourceText: '路径待命',
|
||||||
|
simChannelId: 'default',
|
||||||
currentLatLng: L.latLng(DEFAULT_CENTER[0], DEFAULT_CENTER[1]),
|
currentLatLng: L.latLng(DEFAULT_CENTER[0], DEFAULT_CENTER[1]),
|
||||||
headingDeg: 0,
|
headingDeg: 0,
|
||||||
currentSegmentIndex: 0,
|
currentSegmentIndex: 0,
|
||||||
@@ -128,6 +135,7 @@
|
|||||||
trackFileInput: document.getElementById('trackFileInput'),
|
trackFileInput: document.getElementById('trackFileInput'),
|
||||||
importTrackBtn: document.getElementById('importTrackBtn'),
|
importTrackBtn: document.getElementById('importTrackBtn'),
|
||||||
connectBtn: document.getElementById('connectBtn'),
|
connectBtn: document.getElementById('connectBtn'),
|
||||||
|
simChannelIdInput: document.getElementById('simChannelIdInput'),
|
||||||
sendOnceBtn: document.getElementById('sendOnceBtn'),
|
sendOnceBtn: document.getElementById('sendOnceBtn'),
|
||||||
streamBtn: document.getElementById('streamBtn'),
|
streamBtn: document.getElementById('streamBtn'),
|
||||||
stopStreamBtn: document.getElementById('stopStreamBtn'),
|
stopStreamBtn: document.getElementById('stopStreamBtn'),
|
||||||
@@ -164,6 +172,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
elements.configUrlInput.value = DEFAULT_CONFIG_URL
|
elements.configUrlInput.value = DEFAULT_CONFIG_URL
|
||||||
|
applySimChannelId(loadSimChannelId(), false)
|
||||||
|
|
||||||
function createTileLayer(urlTemplate, extraOptions) {
|
function createTileLayer(urlTemplate, extraOptions) {
|
||||||
return L.tileLayer(urlTemplate, Object.assign({
|
return L.tileLayer(urlTemplate, Object.assign({
|
||||||
@@ -177,6 +186,33 @@
|
|||||||
elements.log.textContent = `[${time}] ${message}\n` + elements.log.textContent
|
elements.log.textContent = `[${time}] ${message}\n` + elements.log.textContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadSimChannelId() {
|
||||||
|
try {
|
||||||
|
return normalizeSimChannelId(window.localStorage.getItem(SIM_CHANNEL_STORAGE_KEY))
|
||||||
|
} catch (_error) {
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSimChannelId(channelId) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(SIM_CHANNEL_STORAGE_KEY, normalizeSimChannelId(channelId))
|
||||||
|
} catch (_error) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySimChannelId(channelId, persist) {
|
||||||
|
state.simChannelId = normalizeSimChannelId(channelId)
|
||||||
|
if (elements.simChannelIdInput) {
|
||||||
|
elements.simChannelIdInput.value = state.simChannelId
|
||||||
|
}
|
||||||
|
if (persist) {
|
||||||
|
saveSimChannelId(state.simChannelId)
|
||||||
|
}
|
||||||
|
renderDebugLog()
|
||||||
|
}
|
||||||
|
|
||||||
function logDebug(entry) {
|
function logDebug(entry) {
|
||||||
if (!elements.debugLog) {
|
if (!elements.debugLog) {
|
||||||
return
|
return
|
||||||
@@ -184,6 +220,7 @@
|
|||||||
|
|
||||||
const normalized = {
|
const normalized = {
|
||||||
timestamp: entry.timestamp || Date.now(),
|
timestamp: entry.timestamp || Date.now(),
|
||||||
|
channelId: normalizeSimChannelId(entry.channelId),
|
||||||
scope: String(entry.scope || 'app'),
|
scope: String(entry.scope || 'app'),
|
||||||
level: String(entry.level || 'info'),
|
level: String(entry.level || 'info'),
|
||||||
message: String(entry.message || ''),
|
message: String(entry.message || ''),
|
||||||
@@ -231,12 +268,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filteredEntries = state.debugLogEntries.filter((entry) => {
|
const filteredEntries = state.debugLogEntries.filter((entry) => {
|
||||||
|
if (normalizeSimChannelId(entry.channelId) !== state.simChannelId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return state.debugLogScopeFilter === 'all' || entry.scope === state.debugLogScopeFilter
|
return state.debugLogScopeFilter === 'all' || entry.scope === state.debugLogScopeFilter
|
||||||
})
|
})
|
||||||
|
|
||||||
if (elements.debugLogMeta) {
|
if (elements.debugLogMeta) {
|
||||||
const scopeLabel = state.debugLogScopeFilter === 'all' ? '全部' : state.debugLogScopeFilter
|
const scopeLabel = state.debugLogScopeFilter === 'all' ? '全部' : state.debugLogScopeFilter
|
||||||
elements.debugLogMeta.textContent = `${scopeLabel} · ${filteredEntries.length} 条`
|
elements.debugLogMeta.textContent = `通道 ${state.simChannelId} · ${scopeLabel} · ${filteredEntries.length} 条`
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.debugLog.textContent = filteredEntries
|
elements.debugLog.textContent = filteredEntries
|
||||||
@@ -244,7 +284,7 @@
|
|||||||
const time = new Date(entry.timestamp || Date.now()).toLocaleTimeString()
|
const time = new Date(entry.timestamp || Date.now()).toLocaleTimeString()
|
||||||
const level = String(entry.level || 'info').toUpperCase()
|
const level = String(entry.level || 'info').toUpperCase()
|
||||||
const payloadText = entry.payload ? ` ${JSON.stringify(entry.payload)}` : ''
|
const payloadText = entry.payload ? ` ${JSON.stringify(entry.payload)}` : ''
|
||||||
return `[${time}] [${entry.scope}] [${level}] ${entry.message}${payloadText}`
|
return `[${time}] [${entry.channelId}] [${entry.scope}] [${level}] ${entry.message}${payloadText}`
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
@@ -1389,6 +1429,7 @@
|
|||||||
const payload = {
|
const payload = {
|
||||||
type: 'mock_gps',
|
type: 'mock_gps',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
channelId: state.simChannelId,
|
||||||
lat: Number(state.currentLatLng.lat.toFixed(6)),
|
lat: Number(state.currentLatLng.lat.toFixed(6)),
|
||||||
lon: Number(state.currentLatLng.lng.toFixed(6)),
|
lon: Number(state.currentLatLng.lng.toFixed(6)),
|
||||||
accuracyMeters: getAccuracy(),
|
accuracyMeters: getAccuracy(),
|
||||||
@@ -1409,6 +1450,7 @@
|
|||||||
const payload = {
|
const payload = {
|
||||||
type: 'mock_heart_rate',
|
type: 'mock_heart_rate',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
channelId: state.simChannelId,
|
||||||
bpm: state.heartRateSampleMode ? getSampleHeartRateBpm() : getHeartRateBpm(),
|
bpm: state.heartRateSampleMode ? getSampleHeartRateBpm() : getHeartRateBpm(),
|
||||||
}
|
}
|
||||||
state.heartRateSocket.send(JSON.stringify(payload))
|
state.heartRateSocket.send(JSON.stringify(payload))
|
||||||
@@ -1840,6 +1882,12 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
elements.connectBtn.addEventListener('click', connectSocket)
|
elements.connectBtn.addEventListener('click', connectSocket)
|
||||||
|
if (elements.simChannelIdInput) {
|
||||||
|
elements.simChannelIdInput.addEventListener('change', () => {
|
||||||
|
applySimChannelId(elements.simChannelIdInput.value, true)
|
||||||
|
log(`已切换模拟通道 ${state.simChannelId}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
elements.applyGatewayBridgePresetBtn.addEventListener('click', applyBridgePresetToForm)
|
elements.applyGatewayBridgePresetBtn.addEventListener('click', applyBridgePresetToForm)
|
||||||
elements.saveGatewayBridgePresetBtn.addEventListener('click', saveCurrentBridgePreset)
|
elements.saveGatewayBridgePresetBtn.addEventListener('click', saveCurrentBridgePreset)
|
||||||
elements.deleteGatewayBridgePresetBtn.addEventListener('click', deleteSelectedBridgePreset)
|
elements.deleteGatewayBridgePresetBtn.addEventListener('click', deleteSelectedBridgePreset)
|
||||||
|
|||||||
@@ -1,231 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Mock GPS Simulator v1</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
|
||||||
<link rel="stylesheet" href="../style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="layout">
|
|
||||||
<aside class="panel">
|
|
||||||
<div class="panel__header">
|
|
||||||
<div class="panel__eyebrow">MOCK GPS SIM</div>
|
|
||||||
<h1>外部模拟器</h1>
|
|
||||||
<div class="panel__links"><a href="/">打开新版工作台</a></div>
|
|
||||||
<div id="socketStatus" class="badge badge--muted">未连接</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">资源加载</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>游戏配置 URL</span>
|
|
||||||
<input id="configUrlInput" type="text" value="https://oss-mbh5.colormaprun.com/wxmini/test/game.json">
|
|
||||||
</label>
|
|
||||||
<div class="row">
|
|
||||||
<button id="loadConfigBtn" class="btn btn--primary">载入配置</button>
|
|
||||||
<button id="fitCourseBtn" class="btn">适配视野</button>
|
|
||||||
</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>瓦片模板</span>
|
|
||||||
<input id="tileUrlInput" type="text" placeholder="https://host/tiles/{z}/{x}/{y}.webp">
|
|
||||||
</label>
|
|
||||||
<div class="row">
|
|
||||||
<button id="applyTilesBtn" class="btn">应用瓦片</button>
|
|
||||||
<button id="resetTilesBtn" class="btn">恢复 OSM</button>
|
|
||||||
</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>KML URL</span>
|
|
||||||
<input id="courseUrlInput" type="text" placeholder="https://host/course/c01.kml">
|
|
||||||
</label>
|
|
||||||
<div class="row">
|
|
||||||
<button id="loadCourseBtn" class="btn">载入控制点</button>
|
|
||||||
<button id="clearCourseBtn" class="btn">清空控制点</button>
|
|
||||||
</div>
|
|
||||||
<div id="resourceStatus" class="hint">支持直接载入 game.json,也支持单独填瓦片模板和 KML 地址。</div>
|
|
||||||
<div id="resourceDetail" class="group__status">尚未载入资源</div>
|
|
||||||
<div id="courseJumpList" class="jump-list"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">实时发送</div>
|
|
||||||
<div id="realtimeStatus" class="group__status">桥接未连接</div>
|
|
||||||
<div id="lastSendStatus" class="group__status">最近发送: --</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="connectBtn" class="btn btn--primary">连接桥接</button>
|
|
||||||
<button id="sendOnceBtn" class="btn">发送一次</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="streamBtn" class="btn btn--accent">开始连续发送</button>
|
|
||||||
<button id="stopStreamBtn" class="btn">停止发送</button>
|
|
||||||
</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>发送频率</span>
|
|
||||||
<select id="hzSelect">
|
|
||||||
<option value="2">2 Hz</option>
|
|
||||||
<option value="5" selected>5 Hz</option>
|
|
||||||
<option value="10">10 Hz</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>精度 (m)</span>
|
|
||||||
<input id="accuracyInput" type="number" min="1" max="100" value="6">
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">新网关桥接</div>
|
|
||||||
<div id="gatewayBridgeStatus" class="group__status">未启用</div>
|
|
||||||
<div id="gatewayBridgeTarget" class="group__status">目标设备: --</div>
|
|
||||||
<div id="gatewayBridgeLast" class="group__status">最近状态: --</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>桥接预设</span>
|
|
||||||
<select id="gatewayBridgePresetSelect">
|
|
||||||
<option value="">选择预设</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>预设名称</span>
|
|
||||||
<input id="gatewayBridgePresetNameInput" type="text" placeholder="例如:家长端-A / 场控-B">
|
|
||||||
</label>
|
|
||||||
<div class="row">
|
|
||||||
<button id="applyGatewayBridgePresetBtn" class="btn">套用预设</button>
|
|
||||||
<button id="saveGatewayBridgePresetBtn" class="btn">保存预设</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="deleteGatewayBridgePresetBtn" class="btn">删除预设</button>
|
|
||||||
</div>
|
|
||||||
<label class="field field--check">
|
|
||||||
<input id="gatewayBridgeEnabledInput" type="checkbox">
|
|
||||||
<span>启用新网关桥接</span>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>网关地址</span>
|
|
||||||
<input id="gatewayBridgeUrlInput" type="text" placeholder="ws://127.0.0.1:18080/ws">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Producer Token / Channel Token</span>
|
|
||||||
<input id="gatewayBridgeTokenInput" type="text" placeholder="producerToken 或 dev-producer-token">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Channel ID</span>
|
|
||||||
<input id="gatewayBridgeChannelIdInput" type="text" placeholder="ch-xxxx">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>目标 Device ID</span>
|
|
||||||
<input id="gatewayBridgeDeviceIdInput" type="text" placeholder="child-001">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>目标 Group ID</span>
|
|
||||||
<input id="gatewayBridgeGroupIdInput" type="text" placeholder="class-a">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Source ID</span>
|
|
||||||
<input id="gatewayBridgeSourceIdInput" type="text" placeholder="mock-gps-sim">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Source Mode</span>
|
|
||||||
<input id="gatewayBridgeSourceModeInput" type="text" placeholder="mock">
|
|
||||||
</label>
|
|
||||||
<div class="row">
|
|
||||||
<button id="applyGatewayBridgeConfigBtn" class="btn btn--primary">应用桥接配置</button>
|
|
||||||
<button id="reloadGatewayBridgeConfigBtn" class="btn">重新读取</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">心率模拟</div>
|
|
||||||
<div id="heartRateStatus" class="group__status">心率模拟待命</div>
|
|
||||||
<div id="lastHeartRateStatus" class="group__status">最近发送: --</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="sendHeartRateOnceBtn" class="btn">发送一次</button>
|
|
||||||
<button id="startHeartRateStreamBtn" class="btn btn--accent">开始连续发送</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="stopHeartRateStreamBtn" class="btn">停止发送</button>
|
|
||||||
<button id="applyHeartRatePresetBtn" class="btn">应用分区样本</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="toggleHeartRateSampleBtn" class="btn">模拟真实样本</button>
|
|
||||||
</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>心率值 (bpm)</span>
|
|
||||||
<input id="heartRateInput" type="number" min="40" max="220" value="120">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>发送频率</span>
|
|
||||||
<select id="heartRateHzSelect">
|
|
||||||
<option value="1" selected>1 Hz</option>
|
|
||||||
<option value="2">2 Hz</option>
|
|
||||||
<option value="4">4 Hz</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>样本模板</span>
|
|
||||||
<select id="heartRateSampleTemplateSelect">
|
|
||||||
<option value="jog" selected>慢跑样本</option>
|
|
||||||
<option value="tempo">节奏跑样本</option>
|
|
||||||
<option value="interval">间歇跑样本</option>
|
|
||||||
<option value="recovery">恢复走样本</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">路径回放</div>
|
|
||||||
<div id="playbackStatus" class="group__status">路径待命</div>
|
|
||||||
<input id="trackFileInput" class="file-input-hidden" type="file" accept=".gpx,.kml,.geojson,.json,application/json,application/gpx+xml,application/vnd.google-earth.kml+xml">
|
|
||||||
<div class="row">
|
|
||||||
<button id="importTrackBtn" class="btn">导入轨迹文件</button>
|
|
||||||
<button id="togglePathModeBtn" class="btn">开启路径编辑</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="clearPathBtn" class="btn">清空路径</button>
|
|
||||||
<button id="fitPathBtn" class="btn">适配路径</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="playPathBtn" class="btn btn--accent">开始回放</button>
|
|
||||||
<button id="pausePathBtn" class="btn">暂停回放</button>
|
|
||||||
</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>移动速度 (km/h)</span>
|
|
||||||
<input id="speedInput" type="number" min="1" max="25" step="0.1" value="6">
|
|
||||||
</label>
|
|
||||||
<label class="field field--check">
|
|
||||||
<input id="loopPathInput" type="checkbox" checked>
|
|
||||||
<span>循环回放</span>
|
|
||||||
</label>
|
|
||||||
<div id="pathHint" class="hint">点击“开启路径编辑”后,在地图上逐点添加路径。</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">当前位置</div>
|
|
||||||
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
|
|
||||||
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
|
|
||||||
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
|
|
||||||
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">日志</div>
|
|
||||||
<div id="log" class="log"></div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main class="map-shell">
|
|
||||||
<div id="map"></div>
|
|
||||||
<section class="floating-debug-log">
|
|
||||||
<div class="floating-debug-log__header">
|
|
||||||
<div class="floating-debug-log__title">调试日志</div>
|
|
||||||
<button id="clearDebugLogBtn" class="floating-debug-log__clear" type="button">清空</button>
|
|
||||||
</div>
|
|
||||||
<div id="debugLog" class="log log--debug log--floating"></div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
||||||
<script src="../simulator.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -48,6 +48,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-topbar__links a {
|
.wb-topbar__links a {
|
||||||
@@ -63,6 +64,26 @@ body {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wb-topbar__global {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar__field {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar__field span {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #5d786c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar__field input {
|
||||||
|
min-width: 220px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
.wb-connection-bar {
|
.wb-connection-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -104,7 +125,7 @@ body {
|
|||||||
.wb-layout {
|
.wb-layout {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 380px 1fr;
|
grid-template-columns: 380px 1fr 280px;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
@@ -115,6 +136,12 @@ body {
|
|||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wb-rail {
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.wb-stage {
|
.wb-stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -123,14 +150,6 @@ body {
|
|||||||
box-shadow: 0 28px 60px rgba(20, 41, 31, 0.18);
|
box-shadow: 0 28px 60px rgba(20, 41, 31, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-bottom-strip {
|
|
||||||
padding: 0 18px 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wb-card--bottom .log {
|
|
||||||
max-height: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
#map {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -515,7 +534,7 @@ body {
|
|||||||
|
|
||||||
@media (max-width: 1380px) {
|
@media (max-width: 1380px) {
|
||||||
.wb-layout {
|
.wb-layout {
|
||||||
grid-template-columns: 340px 1fr;
|
grid-template-columns: 340px 1fr 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-debug-log {
|
.floating-debug-log {
|
||||||
@@ -526,10 +545,11 @@ body {
|
|||||||
@media (max-width: 1120px) {
|
@media (max-width: 1120px) {
|
||||||
.wb-layout {
|
.wb-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: auto minmax(420px, 1fr);
|
grid-template-rows: auto minmax(420px, 1fr) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-sidebar {
|
.wb-sidebar,
|
||||||
|
.wb-rail {
|
||||||
max-height: 32vh;
|
max-height: 32vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,7 +569,9 @@ body {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wb-bottom-strip {
|
.wb-topbar__global,
|
||||||
padding-top: 18px;
|
.wb-topbar__field input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user